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.
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.
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.
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.
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.
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.
In the latest release of RxJs, the flatMap
operator has been renamed to mergeMap
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With