Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handling refresh tokens using rxjs

Tags:

angular

rxjs

Since i've started with angular2 i have setup my services to return Observable of T. In the service i would have the map() call, and components using these services would just use subscribe() to wait for the response. For these simple scenarios i didnt really need to dig in to rxjs so all was ok.

I now want to achieve the following: i am using Oauth2 authentication with refresh tokens. I want to build an api service that all other services will use, and that will transparently handle the refresh token when a 401 error is returned. So, in the case of a 401, i first fetch a new token from the OAuth2 endpoint, and then retry my request with the new token. Below is the code that works fine, with promises:

request(url: string, request: RequestOptionsArgs): Promise<Response> {     var me = this;      request.headers = request.headers || new Headers();     var isSecureCall: boolean =  true; //url.toLowerCase().startsWith('https://');     if (isSecureCall === true) {         me.authService.setAuthorizationHeader(request.headers);     }     request.headers.append('Content-Type', 'application/json');     request.headers.append('Accept', 'application/json');      return this.http.request(url, request).toPromise()         .catch(initialError => {             if (initialError && initialError.status === 401 && isSecureCall === true) {                 // token might be expired, try to refresh token.                  return me.authService.refreshAuthentication().then((authenticationResult:AuthenticationResult) => {                     if (authenticationResult.IsAuthenticated == true) {                         // retry with new token                         me.authService.setAuthorizationHeader(request.headers);                         return this.http.request(url, request).toPromise();                     }                     return <any>Promise.reject(initialError);                 });             }             else {                 return <any>Promise.reject(initialError);             }         }); } 

In the code above, authService.refreshAuthentication() will fetch the new token and store it in localStorage. authService.setAuthorizationHeader will set the 'Authorization' header to previously updated token. If you look at the catch method, you'll see that it returns a promise (for the refresh token) that in its turns will eventually return another promise (for the actual 2nd try of the request).

I have attempted to do this without resorting to promises:

request(url: string, request: RequestOptionsArgs): Observable<Response> {     var me = this;      request.headers = request.headers || new Headers();     var isSecureCall: boolean =  true; //url.toLowerCase().startsWith('https://');     if (isSecureCall === true) {         me.authService.setAuthorizationHeader(request.headers);     }     request.headers.append('Content-Type', 'application/json');     request.headers.append('Accept', 'application/json');      return this.http.request(url, request)         .catch(initialError => {             if (initialError && initialError.status === 401 && isSecureCall === true) {                 // token might be expired, try to refresh token                 return me.authService.refreshAuthenticationObservable().map((authenticationResult:AuthenticationResult) => {                     if (authenticationResult.IsAuthenticated == true) {                         // retry with new token                         me.authService.setAuthorizationHeader(request.headers);                         return this.http.request(url, request);                     }                     return Observable.throw(initialError);                 });             }             else {                 return Observable.throw(initialError);             }         }); } 

The code above does not do what i expect: in the case of a 200 response, it properly returns the response. However, if it catches the 401, it will successfully retrieve the new token, but the subscribe wil eventually retrieve an observable instead of the response. Im guessing this is the unexecuted Observable that should do the retry.

I realize that translating the promise way of working onto the rxjs library is probably not the best way to go, but i havent been able to grasp the "everything is a stream" thing. I have tried a few other solutions involving flatmap, retryWhen etc ... but didnt get far, so some help is appreciated.

like image 756
Davy Avatar asked Jan 20 '16 17:01

Davy


People also ask

How do you handle refresh token?

Refresh Tokens are credentials used to obtain access tokens. Refresh tokens are issued to the client by the authorization server and are used to obtain a new access token when the current access token becomes invalid or expires, or to obtain additional access tokens with identical or narrower scope.

Where should you store refresh token values?

Storing tokens in browser local storage provides persistence across page refreshes and browser tabs; however, if malicious users managed to run JavaScript in the SPA using a cross-site scripting (XSS) attack, they could retrieve the tokens stored in local storage.

When should refresh token be refreshed?

The sliding window is 30 days or the remaining consent window — whichever is less. This means that even if an authorisation consent is, say, 90 days, the refresh token must be used within a 30-day window in order to “refresh” the access — at which point the refresh token lifetime is reset once again.

Can refresh token be stored in cookie?

Store your access token in memory, and store the refresh token in the cookie: Link to this section. Why is this safe from CSRF? Yes, a form submit to /refresh_token would work and a new access token will be returned, but the attacker can't read the response if they're using an HTML form.


2 Answers

From a quick look at your code I would say that your problem seems to be that you are not flattening the Observable that is returned from the refresh service.

The catch operator expects that you will return an Observable that it will concatenate onto the end of the failed Observable so that the down stream Observer doesn't know the difference.

In the non-401 case you are doing this correctly by returning an Observable that rethrows the initial error. However in the refresh case you are returning an Observable the produces more Observables instead of single values.

I would suggest you change the refresh logic to be:

    return me.authService              .refreshAuthenticationObservable()              //Use flatMap instead of map              .flatMap((authenticationResult:AuthenticationResult) => {                    if (authenticationResult.IsAuthenticated == true) {                      // retry with new token                      me.authService.setAuthorizationHeader(request.headers);                      return this.http.request(url, request);                    }                    return Observable.throw(initialError);     }); 

flatMap will convert the intermediate Observables into a single stream.

like image 87
paulpdaniels Avatar answered Oct 15 '22 07:10

paulpdaniels


In the latest release of RxJs, the flatMap operator has been renamed to mergeMap.

like image 32
mostefaiamine Avatar answered Oct 15 '22 07:10

mostefaiamine