2

Using the [ServiceStack Typescript client][1] and ServiceStack Auth on the backend I am seeing a failure to call/access-token` after an initial API request that receives a HTTP 401 response in Microsoft Edge.

It seems that Microsoft Edge may handle the HTTP Exception and thus ServiceStack client never gets a "notification" (the notification being the 401 result) and thus never has the opportunity to handle the 401 response and call <my_servicestack_api_auth_url>/access-token to get a bearer token before trying the API request again.

You can see filtered Network stack requests in Chrome:

enter image description here

Now look at the filtered Network stack in Microsoft Edge:

enter image description here

And here is Microsoft Edge Console Window:

enter image description here

You can see that in Microsoft Edge, no <my_servicestack_api_auth_url>/access-token requests are ever made after 401 responses to initial requests.

Is ir possible that ServiceStack is not making requests to <my_servicestack_api_auth_url>/access-token and then subsequently not retrying the initial API request because of Microsoft Edge handling the 401 error itself?

Brian Ogden
  • 18,439
  • 10
  • 97
  • 176
  • Is there some implied question(s) in this post? – R. Richards Apr 12 '18 at 22:41
  • @R.Richards to someone who uses ServiceStack the question is a bit more apparent, I edited my question to add a question :) – Brian Ogden Apr 13 '18 at 00:24
  • @BrianOgden are both these screenshots the same? either way it does appear that MS Edge is hijacking the 401 error response and preventing ServiceStack from handling it. I'm assuming there's no JavaScript errors in the console? – mythz Apr 13 '18 at 00:32
  • Hi @mythz, yes sorry both screenshots are the same, I have updated my question and replaced the second screenshot with Microsoft Edge network stack screenshot. You are correct, no Javascript error, I included a screenshot of Microsoft Edge Console for you was well – Brian Ogden Apr 13 '18 at 00:37
  • Hi @mythz, you had a change to check out this issue yet? – Brian Ogden Apr 16 '18 at 17:56
  • @BrianOgden From your description it sounds like MS Edge is hijacking the 401 error response preventing the JsonServiceClient from being able to handle it. Although I'm not able to repro this error locally, our test suite passes in MS Edge including [our refreshToken tests](https://github.com/ServiceStack/servicestack-client/blob/d954990050c018bd67b16a452416838071accc79/tests/client.auth.spec.ts#L139). – mythz Apr 16 '18 at 18:09
  • @mythz Isn't the JsonServiceClient need to know about the 401 expectation so that it can properly make a request to the auth api before trying the request again? Tests are passing for Edge doing this? – Brian Ogden Apr 17 '18 at 21:23
  • @BrianOgden Yes it needs to handle the 401 Response in order to be able to fetch a new JWT using the refreshToken. All browsers run the same impl which does this. – mythz Apr 17 '18 at 21:26
  • @mythz I do not understand how your test suite passes for MS Edge then, wouldn't JsonServiceClient tests fail if Edge is hijacking the 401? – Brian Ogden Apr 17 '18 at 22:50
  • @BrianOgden I'm just going off your description, you may be hitting a scenario where MS Edge is preventing handling 401's but I don't actually know what's causing your issue since I'm unable to reproduce it. – mythz Apr 17 '18 at 22:55
  • @mythz, well let me double check something here, your test suite is testing against your test auth api direct. My scenario is when, after authenticating, and calling another api, with just the refresh token, the hijacked 401 prevents the JsonServiceClient from getting a bearer token and retrying the other api request, do you agree with this logic? – Brian Ogden Apr 17 '18 at 23:02
  • I [linked to the test that uses the refreshToken](https://github.com/ServiceStack/servicestack-client/blob/d954990050c018bd67b16a452416838071accc79/tests/client.auth.spec.ts#L139), not sure what you mean by direct, it's initially using an expired JWT which fails with the 401 that the client then uses the refreshToken to fetch a new JWT which it uses to authenticate with. The test is run from `http://localhost:8080` which authenticates against the `http://test.servicestack.net` Server. – mythz Apr 17 '18 at 23:14
  • @mythz how do you run servicestack-client on localhost:8080 to run your test suite in a specific browser using the testrunner.html file? I can run via "npm run test" obviously but you do not have a "serve" command or the likes – Brian Ogden Apr 18 '18 at 19:34
  • @BrianOgden Install `npm i -g http-server` then you can run [http-sever](https://www.npmjs.com/package/http-server) from the base project folder. – mythz Apr 18 '18 at 19:40
  • @myth thanks, tests run fine in Chrome,testrunner.html does not run at all in Microsoft Edge, Javascript stack errors, I have added ES5 and 6 shims and still seeing "TypeError: Object doesn't support this action at Anonymous function tests/client.spec.js:67:9)". I really need to do a sanity check that the JsonServiceClient still gets a new bearer token after a 401 in Microsoft Edge – Brian Ogden Apr 18 '18 at 22:53
  • @BrianOgden You're likely just be hitting a timeout when the test exceeds 2s. Here's a [screenshot of all tests passing in MS Edge](https://imgur.com/a/ZUR49) – mythz Apr 18 '18 at 23:04
  • @mythz thanks for the sanity check, I guess I needed that to keep questioning my code and not yours, I found the beginnings of my problem, I will probably be deleting my question soon, because I do not think Edge is hijacking the 401 anymore, I think I am :) – Brian Ogden Apr 18 '18 at 23:41
  • Don't delete the question, just add an answer saying what the issue and solution was which can help others hitting the same issue in future. – mythz Apr 18 '18 at 23:49

1 Answers1

1

I extended the servicestack-client to wrap up handling exceptions, token storage, rejections. I was not rethrowing exceptions (though this only had a side effect in Edge, not Chrome or IE11) in my exception handler and thus was hiding what the actual error which is here.

import { JsonServiceClient, IReturn, ErrorResponse } from '@servicestack/client';
//service
import { AuthService } from './api/auth.service';
import { SpinnerService } from '../spinner/spinner.service';
//dtos
import { GetAccessToken, ConvertSessionToToken, ConvertSessionToTokenResponse } from './api/dtos'
import { AppModule } from '../../app.module';
import { Router } from '@angular/router';
import { TokenService } from './token.service';
import { ApiHelper } from '../api/api.helper';
import { AppRoutes } from '../const/routes/app-routes.const';

export class JsonServiceClientAuth extends JsonServiceClient {

    private router: Router;
    private tokenService: TokenService;
    private apiHelper: ApiHelper;
    private spinnerService: SpinnerService;

    constructor(baseUrl: string) {
        super(baseUrl);

        //Router, TokenService, ApiHelper are not injected via the contructor because clients of JsonServiceClientAuth need to create instances of it as simple as possibl
        this.router = AppModule.injector.get(Router);
        this.tokenService = AppModule.injector.get(TokenService);
        this.apiHelper = AppModule.injector.get(ApiHelper);
        this.spinnerService = AppModule.injector.get(SpinnerService);

        //refresh token 
        //http://docs.servicestack.net/jwt-authprovider#using-an-alternative-jwt-server
        this.refreshTokenUri = this.apiHelper.getServiceUrl(this.apiHelper.ServiceNames.auth) + "/access-token";

        this.onAuthenticationRequired = async () => {
            this.redirectToLogin();
        };
    }

    get<T>(request: IReturn<T> | string, args?: any): Promise<T> {
        this.prepareForRequest();

        let promise = new Promise<T>((resolve, reject) => {
            super.get(request)
                .then(res => {
                    this.handleSuccessfulResponse();
                    resolve(res);
                }, msg => {
                    this.handleCompletion();
                    this.handleRejection(msg);
                    reject(msg);
                })
                .catch(ex => this.handleCompletion(ex))
        });

        return promise;
    }

    post<T>(request: IReturn<T>, args?: any): Promise<T> {
        this.prepareForRequest();

        let promise = new Promise<T>((resolve, reject) => {
            super.post(request)
                .then(res => {
                    this.handleSuccessfulResponse();
                    resolve(res);
                }, msg => {
                    this.handleCompletion();
                    this.handleRejection(msg);
                    reject(msg);
                })
                .catch(ex => this.handleCompletion(ex))
        });

        return promise;
    }

    put<T>(request: IReturn<T>, args?: any): Promise<T> {
        this.prepareForRequest();

        let promise = new Promise<T>((resolve, reject) => {
            super.put(request)
                .then(res => {
                    this.handleSuccessfulResponse();
                    resolve(res);
                }, msg => {
                    this.handleCompletion();
                    this.handleRejection(msg);
                    reject(msg);
                })
                .catch(ex => this.handleCompletion(ex))
        });

        return promise;
    }

    delete<T>(request: IReturn<T>, args?: any): Promise<T> {
        this.prepareForRequest();

        let promise = new Promise<T>((resolve, reject) => {
            super.delete(request)
                .then(res => {
                    this.handleSuccessfulResponse();
                    resolve(res);
                }, msg => {
                    this.handleCompletion();
                    this.handleRejection(msg);
                    reject(msg);
                })
                .catch(ex => this.handleCompletion(ex))
        });

        return promise;
    }

    private handleRefreshTokenException() {
        this.redirectToLogin();
    }

    private handleCompletion(ex: any = null) {
        //hide spinner in case it was showing
        this.spinnerService.display(false);
        if(ex) {
            console.log('JsonServiceClientAuth.handleCompletion: rethrowing exception', ex);
            throw ex;
        }
    }

    private handleRejection(msg: any) {
        if (msg == "TypeError: Failed to fetch"){
            console.error('Failed to fetch: IT IS QUITE POSSIBLE THAT THE API YOU ARE CALLING IS DOWN');
        } 

        if(msg.responseStatus && msg.responseStatus.errorCode === "401"){
            //an API has rejected the request
            console.log('JsonServiceClientAuth.handleRejection: API returned 401 redirect to not authorized');
            this.router.navigate(['/', AppRoutes.NotAuthorized]);
        }

        if (msg.type === "RefreshTokenException") {
            console.log('JsonServiceClientAuth.handleRejection: there was a token refresh exeception');
            this.handleRefreshTokenException();
        }
    }

    private redirectToLogin() {
        this.router.navigate(['/', AppRoutes.Login], { queryParams: { redirectTo: this.router.url } });
    }

    /**
     * cross domain resources require that we explicity set the token in ServiceStack JsonServiceClients
     * https://stackoverflow.com/questions/47422212/use-the-jwt-tokens-across-multiple-domains-with-typescript-jsonserviceclient-s
     */
    private prepareForRequest() {
        //console.log('JsonServiceClientAuth.prepareForRequest');
        //console.log('this.tokenService.refreshToken', this.tokenService.refreshToken);
        this.refreshToken = this.tokenService.refreshToken;
        this.bearerToken = this.tokenService.bearerToken;
    }

    /**
     * refresh the bearer token with the latest data, every request is passed with the refresh token and the freshest bearerToken will be
     * returned with every response
     */
    private handleSuccessfulResponse() {
        //console.log('JsonServiceClientAuth.handleSuccessfulResponse');
        //console.log('this.bearerToken', this.bearerToken);
        //this will update the client side bearerToken, keeping it fresher - Ogden 4-18-2018
        this.tokenService.bearerToken = this.bearerToken;
    }
}
Brian Ogden
  • 18,439
  • 10
  • 97
  • 176