angular-auth-oidc-client: checkAuthIncludingServer cannot complete without credentials

Hi, owing to the rewrite of forceRefreshSession (thank you) there are circumstances where the checkAuthIncludingServer and / or forceRefeshSession observables will never return a value. I believe its a race error, but is apparent (for me) if either is subscribed to without any existing credentials in the sessionStorage.

What’s happening -

  1. The silentRenewEventHandler creates the callback to handle the IFrame result via codeFlowCallbackSilentRenewIframe , receives an error from the IFrame querystring because the refresh request didn’t succeed and immediately throws
  2. silentRenewEventHandler catches the error in its subscription to the callback, and updates refreshSessionWithIFrameCompletedInternal$ to null - all correctly
  3. However, all this happens before startRefreshSession() actually returns a result - and because the results are expected consecutively, forceRefreshSession will now wait forever for a result from refreshSessionWithIFrameCompleted$

I guess it may or may not happen depending on speed of network.

I suggest a fix is to deal with the observable concurrently

    return this.startRefreshSession().pipe(
      switchMap(() => this.silentRenewService.refreshSessionWithIFrameCompleted$),
      take(1),
      map((callbackContext) => {
           ...
      })
    );

… to …

    return forkJoin(
        this.startRefreshSession(), 
        this.silentRenewService.refreshSessionWithIFrameCompleted$.pipe(take(1)))
    .pipe(
      map(([callbackContext, _]) => {
           ...
      })
    );

Thank you!

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 1
  • Comments: 20 (4 by maintainers)

Most upvoted comments

@FabianGosebrink @PartyArk @Expelz Thanks for you work. I’ll test this tomorrow, then PR, release, good?

will release in version 11.1.4

Hi guys! As @PartyArk mentioned the problem is occurred here:

return this.startRefreshSession().pipe(
      switchMap(() => this.silentRenewService.refreshSessionWithIFrameCompleted$),
      take(1),
      map((callbackContext) => {
           ...
      })
    );

It happens because next value into refreshSessionWithIFrameCompleted$ will be sent before startRefreshSession() will return observable object.

After long investigation why it happens and in what situations, I finally found root cause of the problem:

  1. startRefreshSession() will execute this function: https://github.com/damienbod/angular-auth-oidc-client/blob/0799173fa19ca803109aa2bf8083361c3806bfa9/projects/angular-auth-oidc-client/src/lib/iframe/refresh-session-iframe.service.ts#L26-L41 This code will register handler for 'load' event and then redirect iframe to IS authorize endpoint - sessionIframe.src = url;
  2. Then only after unsuccessful authorize (authentication cookies expired or doesn’t exist or any other IS error like invalid grant_type etc.) the iframe will be redirected back to SPA special page for silent renew - “https://********/silent-renew.html”.
  3. Inside this page we have inline script with onload handler function, which will dispatch event and after will start execution of silent renew event handler function: https://github.com/damienbod/angular-auth-oidc-client/blob/0799173fa19ca803109aa2bf8083361c3806bfa9/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.ts#L100-L128

And here is occurred main problem - we have two event handlers for 'load' event for iframe. Event handler from inline script (silent-renew.html) will be executed first. And given that the authorization was unsuccessful (step 2), execution of this 'load' event handler will be finished after throw error inside silent renew service: https://github.com/damienbod/angular-auth-oidc-client/blob/0799173fa19ca803109aa2bf8083361c3806bfa9/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.ts#L56-L73

Than this error will be handled by subscriber: https://github.com/damienbod/angular-auth-oidc-client/blob/0799173fa19ca803109aa2bf8083361c3806bfa9/projects/angular-auth-oidc-client/src/lib/iframe/silent-renew.service.ts#L117-L127

Finally here we see call to refreshSessionWithIFrameCompleted$.next(). And now pay attention that all this actions happened before startRefreshSession() will return observable object.

Reasonable question: “Why doesn’t this happen with successful authorization (silent renew)?” The answer: because after success authorization our iframe will do code exchange (code from IS which we should exchange to get token(s)). And it will be done by httpClient through post request it allows to switch execution context to second 'onload' handler (from step 1) which finally will return observable object startRefreshSession() before result from refreshSessionWithIFrameCompleted$.

I agree with @PartyArk forkJoin will handle it. If you want reproduce such behavior you need clear not only local or sessions storage but also Cookies (there is located authentication cookie from IS).

I’ve had another look at this - I have tried forcing karma to launch a browser with third-party-cookies-disabled but I’m not really sure how. Nor do I know how I would write a test, sorry.

However, the fix is really simple - there’s a glitch in the forkJoin I proposed and you said you’d tried too. The returned array parameters are in the wrong order. Should be:

    return forkJoin(
        this.startRefreshSession(),
        this.silentRenewService.refreshSessionWithIFrameCompleted$.pipe(take(1)))
        .pipe(
            map(([_, callbackContext]) => {
                   ....
            })
        );

All tests pass with this in place, and my “manual” testing indicates that the hanging silent-renew bug goes away too. But I think it would be better to have some proper tests in place.