angular-auth-oidc-client icon indicating copy to clipboard operation
angular-auth-oidc-client copied to clipboard

Bug: id token is erased when the server does not send a new idtoken on refresh

Open agardiol opened this issue 2 years ago • 18 comments

What Version of the library are you using? 13.1.0

Question I am trying to get refresh process working with a server that give no id token with the refresh answer:

{access_token: "ZEPGAiCD01MSG5qleO9gQzjD46c9D9", expires_in: 60, token_type: "Bearer",…}
access_token: "ZEPGAiCD01MSG5qleO9gQzjD46c9D9"
expires_in: 60
refresh_token: "hc3Crg96ehnOblKUImHAeGRgamPvf5"
scope: "openid read write"
token_type: "Bearer"

As explained in the documentation, I set the disableRefreshIdTokenAuthTimeValidation option to false and I manually call

this._oidcSecurityService.forceRefreshSession().subscribe((result:any) => {
                  if (!result || result == null) {
                    console.error('Access token refresh process failed -> Logoff requested !');
                    ...
                  }
                });

I always get an error even if I can see in the log: image

Note that access token expiration is set to 60s (for test) and id token expiration to 10s in the server.

agardiol avatar Mar 16 '22 13:03 agardiol

Thanks by advance !

agardiol avatar Mar 16 '22 13:03 agardiol

@damienbod Sorry to insist. Do you have any answer to this question ? Thanks by advance.

agardiol avatar Apr 11 '22 13:04 agardiol

@agardiol This should work, I will test this again , doing a new release now

Greetings Damien

damienbod avatar Apr 17 '22 17:04 damienbod

Update for v14.0: The behavior is still the same with the new version 14. The new access token is well updated in the authResult area in the session storage, but the returned object of forceRefreshSession().subscribe((loginResponse: LoginResponse) is still null even if the process succeed. image

Note also that even if the access_token is updated in the session storage, it is no more injected in the next backend request as authorization Bearer. The refresh process is so not fully successful.

agardiol avatar Apr 19 '22 12:04 agardiol

I have also encountered this issue, here is the culprit: https://github.com/damienbod/angular-auth-oidc-client/blob/9fc89c8a6ce8af4a2abd528c484326a6f0fa5f64/projects/angular-auth-oidc-client/src/lib/flows/callback-handling/history-jwt-keys-callback-handler.service.ts#L34

When id_token is missing in refresh response, existing id_token will be discarded. @damienbod can this be patched by preserving current id_token, or will it break something else?

As a workaround without modifying package code I have patched id_token on save if it suddenly disappears, something like this:

import { Injectable } from '@angular/core';
import { AbstractSecurityStorage } from 'angular-auth-oidc-client';
import * as _ from 'lodash';

@Injectable({
  providedIn: 'root',
})
export class CustomSecurityStorage implements AbstractSecurityStorage {
  read(key: string) {
    return sessionStorage.getItem(key);
  }

  write(key: string, value: any): void {
    const currentDataRaw = sessionStorage.getItem(key);
    if (!currentDataRaw) {
      sessionStorage.setItem(key, value);
      return;
    }

    // Patch id_token when missing in token refresh response
    const currentDataParsed = JSON.parse(currentDataRaw) || {};
    const newDataParsed = JSON.parse(value) || {};

    const currentIdToken = _.get(currentDataParsed, 'authnResult.id_token');
    const newIdToken = _.get(newDataParsed, 'authnResult.id_token');
    const newAccessToken = _.get(newDataParsed, 'authnResult.access_token');

    // If id_token is currently present, but missing from refresh response, patch it with old value
    const newDataPatched =
      newAccessToken && currentIdToken && !newIdToken
        ? _.set(newDataParsed, 'authnResult.id_token', currentIdToken)
        : newDataParsed;

    sessionStorage.setItem(key, JSON.stringify(newDataPatched));
  }

  remove(key: string): void {
    sessionStorage.removeItem(key);
  }

  clear(): void {
    sessionStorage.clear();
  }
}

and then provide in app module

providers: [
    {
      provide: AbstractSecurityStorage,
      useClass: CustomSecurityStorage,
    },
  ],

Do you need help with the patch?

lwalejko avatar May 13 '22 15:05 lwalejko

Thanks for this, looks good and needed. @FabianGosebrink?

Not sure if this is the best way of solving this.

damienbod avatar May 13 '22 18:05 damienbod

Looks like this was given the investigate label back in May. A workaround has been given but it looks like there is some concern. Anything new on this issue or the others that are related to it @damienbod , @FabianGosebrink? I appreciate your time and I hope all is well!

bgerhards avatar Aug 11 '22 16:08 bgerhards

Is it normal or a misconfiguration that when refreshing the token, no ID token is being sent with?

FabianGosebrink avatar Aug 12 '22 06:08 FabianGosebrink

It depends on the type of the backend server. By example a standard Django configuration does not send ID token when refreshing session.

agardiol avatar Aug 12 '22 06:08 agardiol

Another specific example is NetIQ: https://www.netiq.com/documentation/access-manager-45-developer-documentation/oauth-application-developer-guide/data/managing-tokens.html

bgerhards avatar Aug 12 '22 13:08 bgerhards

I will try to setup an IDP to test this. All the IDPs I use, return the id_token

damienbod avatar Aug 12 '22 14:08 damienbod

These 2 IDPs are not certified, are these servers OIDC conform?

https://openid.net/certification/

damienbod avatar Aug 12 '22 14:08 damienbod

Micro Focus Access Manager is apparently another name for it? Micro Focus owns NetIQ. If we are incorrect on Access Manager being another alias, please send correction.

bgerhards avatar Aug 12 '22 14:08 bgerhards

@bgerhards I will try to setup a OIDC Code flow with PKCE and refresh tokens using "Micro Focus Access Manager"

No promises when, never used this IDP before.

damienbod avatar Aug 12 '22 14:08 damienbod

Would the title "id token is erased when the server does not send a new idtoken on refresh" be a better fit?

FabianGosebrink avatar Aug 12 '22 16:08 FabianGosebrink

Is it normal or a misconfiguration that when refreshing the token, no ID token is being sent with? From official OpenID connect core final documention (https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse): ID Token is not mandatory

agardiol avatar Aug 22 '22 10:08 agardiol

FYI This issue is a duplicate of https://github.com/damienbod/angular-auth-oidc-client/issues/1039#issuecomment-979481866 which was closed for no reason at all.

petersandor avatar Sep 02 '22 19:09 petersandor

@bgerhards I will try to setup a OIDC Code flow with PKCE and refresh tokens using "Micro Focus Access Manager"

No promises when, never used this IDP before.

@damienbod I can give you a clientID and a test user in our dev Microfocus access manager system if you would like to test it that way.

jschutte27 avatar Sep 09 '22 20:09 jschutte27

@jschutte27 Yes please and the config to connect as well?

damienbod avatar Sep 23 '22 12:09 damienbod

@damienbod I sent you an email with the details. Let me know if you don't get that or have questions. I forgot to add the scopes to the email, but you can use "openid profile email roles".

jschutte27 avatar Sep 23 '22 13:09 jschutte27

We have the same problem, token endpoint does not return an id token for a refresh token. After an automatic or manual refresh the session is broken.

yasargil avatar Oct 17 '22 13:10 yasargil

Seems like the id_token should be validated only once after the initial login. See: https://stackoverflow.com/a/25810348

yasargil avatar Oct 18 '22 07:10 yasargil

A simple fix would be to store the id_token like the access_token in a separate field.

yasargil avatar Oct 18 '22 08:10 yasargil

Following patch helps us when enableIdTokenExpiredValidationInRenew is false

diff --git a/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.service.ts b/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.service.ts
index 9aa0b6f7..4900c506 100644
--- a/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.service.ts
+++ b/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.service.ts
@@ -54,6 +54,11 @@ export class AuthStateService {
   ): void {
     this.loggerService.logDebug(currentConfig, `storing the accessToken '${accessToken}'`);
 
+    if (authResult.id_token != null) {
+      this.loggerService.logDebug(currentConfig, `storing the id token '${authResult.id_token}'`);
+      this.storagePersistenceService.write('authzIdTokenData', accessToken, currentConfig);
+    }
+
     this.storagePersistenceService.write('authzData', accessToken, currentConfig);
     this.persistAccessTokenExpirationTime(authResult, currentConfig);
     this.setAuthenticatedAndFireEvent(allConfigs);
diff --git a/projects/angular-auth-oidc-client/src/lib/storage/storage-persistence.service.ts b/projects/angular-auth-oidc-client/src/lib/storage/storage-persistence.service.ts
index 5865d5c2..ed3f19d7 100644
--- a/projects/angular-auth-oidc-client/src/lib/storage/storage-persistence.service.ts
+++ b/projects/angular-auth-oidc-client/src/lib/storage/storage-persistence.service.ts
@@ -5,6 +5,7 @@ import { BrowserStorageService } from './browser-storage.service';
 export type StorageKeys =
   | 'authnResult'
   | 'authzData'
+  | 'authzIdTokenData'
   | 'access_token_expires_at'
   | 'authWellKnownEndPoints'a
   | 'userData'
@@ -67,6 +68,7 @@ export class StoragePersistenceService {
 
   resetAuthStateInStorage(config: OpenIdConfiguration): void {
     this.remove('authzData', config);
+    this.remove('authzIdTokenData', config);
     this.remove('reusable_refresh_token', config);
     this.remove('authnResult', config);
   }
@@ -76,7 +78,7 @@ export class StoragePersistenceService {
   }
 
   getIdToken(config: OpenIdConfiguration): string {
-    return this.read('authnResult', config)?.id_token;
+    return this.read('authzIdTokenData', config);
   }
 
   getRefreshToken(config: OpenIdConfiguration): string {

yasargil avatar Oct 18 '22 09:10 yasargil

Following patch helps us when enableIdTokenExpiredValidationInRenew is false

diff --git a/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.service.ts b/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.service.ts
index 9aa0b6f7..4900c506 100644
--- a/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.service.ts
+++ b/projects/angular-auth-oidc-client/src/lib/auth-state/auth-state.service.ts
@@ -54,6 +54,11 @@ export class AuthStateService {
   ): void {
     this.loggerService.logDebug(currentConfig, `storing the accessToken '${accessToken}'`);
 
+    if (authResult.id_token != null) {
+      this.loggerService.logDebug(currentConfig, `storing the id token '${authResult.id_token}'`);
+      this.storagePersistenceService.write('authzIdTokenData', accessToken, currentConfig);
+    }
+
     this.storagePersistenceService.write('authzData', accessToken, currentConfig);
     this.persistAccessTokenExpirationTime(authResult, currentConfig);
     this.setAuthenticatedAndFireEvent(allConfigs);
diff --git a/projects/angular-auth-oidc-client/src/lib/storage/storage-persistence.service.ts b/projects/angular-auth-oidc-client/src/lib/storage/storage-persistence.service.ts
index 5865d5c2..ed3f19d7 100644
--- a/projects/angular-auth-oidc-client/src/lib/storage/storage-persistence.service.ts
+++ b/projects/angular-auth-oidc-client/src/lib/storage/storage-persistence.service.ts
@@ -5,6 +5,7 @@ import { BrowserStorageService } from './browser-storage.service';
 export type StorageKeys =
   | 'authnResult'
   | 'authzData'
+  | 'authzIdTokenData'
   | 'access_token_expires_at'
   | 'authWellKnownEndPoints'a
   | 'userData'
@@ -67,6 +68,7 @@ export class StoragePersistenceService {
 
   resetAuthStateInStorage(config: OpenIdConfiguration): void {
     this.remove('authzData', config);
+    this.remove('authzIdTokenData', config);
     this.remove('reusable_refresh_token', config);
     this.remove('authnResult', config);
   }
@@ -76,7 +78,7 @@ export class StoragePersistenceService {
   }
 
   getIdToken(config: OpenIdConfiguration): string {
-    return this.read('authnResult', config)?.id_token;
+    return this.read('authzIdTokenData', config);
   }
 
   getRefreshToken(config: OpenIdConfiguration): string {

This flag does not seem to work for me. The library just stops adding the access token to the header after the refresh call without id_token.

michal-husak avatar Oct 19 '22 14:10 michal-husak

This only works with the patch, without the id_token is lost after the refresh call.

yasargil avatar Oct 20 '22 08:10 yasargil

I solved it with this interceptor :D

@Injectable()
export class MissingIdTokenDuringRefreshCompensationInterceptor implements HttpInterceptor {

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    if (
      request.url.includes('/token') &&
      request.method === 'POST' &&
      (request.body as string).includes('grant_type=refresh_token')
    ) {
      return next.handle(request).pipe(
        tap((response: any) => {
          if (response.body) {
            response.body.id_token = response.body.id_token ?? response.body.access_token;
          }
        })
      );
    }
    return next.handle(request);
  }
}

as the id_token is not needed in my case anyway I can live with the access_token there as a placeholder.

michal-husak avatar Oct 21 '22 10:10 michal-husak

Basically we can fix this asking for an idtoken in the callback response, or am I missing something?

Like in projects\angular-auth-oidc-client\src\lib\flows\callback-handling\history-jwt-keys-callback-handler.service.ts

 callbackHistoryAndResetJwtKeys(
    callbackContext: CallbackContext,
    config: OpenIdConfiguration,
    allConfigs: OpenIdConfiguration[]
  ): Observable<CallbackContext> {
    if (!this.responseHasIdToken(callbackContext)) {
      const existingIdToken = this.storagePersistenceService.getIdToken(config);

      callbackContext.authResult.id_token = existingIdToken;
    }

    this.storagePersistenceService.write('authnResult', callbackContext.authResult, config);

    /* ... */
  }

Any thoughts?

FabianGosebrink avatar Nov 03 '22 10:11 FabianGosebrink

In our case the idtoken will be expired by then. So if there will be a check after that callback it will fail.

yasargil avatar Nov 03 '22 10:11 yasargil

Basically we can fix this asking for an idtoken in the callback response, or am I missing something?

Like in projects\angular-auth-oidc-client\src\lib\flows\callback-handling\history-jwt-keys-callback-handler.service.ts

 callbackHistoryAndResetJwtKeys(
    callbackContext: CallbackContext,
    config: OpenIdConfiguration,
    allConfigs: OpenIdConfiguration[]
  ): Observable<CallbackContext> {
    if (!this.responseHasIdToken(callbackContext)) {
      const existingIdToken = this.storagePersistenceService.getIdToken(config);

      callbackContext.authResult.id_token = existingIdToken;
    }

    this.storagePersistenceService.write('authnResult', callbackContext.authResult, config);

    /* ... */
  }

Any thoughts?

I was thinking that we could preserve the first original id_token in the sessionStorage after the token refresh call returns an answer without the id_token && if the enableIdTokenExpiredValidationInRenew is set to false.

michal-husak avatar Nov 03 '22 10:11 michal-husak