Skip to content

Silent refresh timeout Authorization code flow with PKCE #1285

Open
@Pieter-1337

Description

@Pieter-1337

Hi,

I am currently updating our flow from implicit to auth code flow with PKCE.
All seems to be working well but I am running in to one strange situation.

Basically every time I trigger a silent refresh (we have a public client, so via the iframe silent-refresh.html), I can see from my logging that the silent refresh is triggered, and that the token is refreshed
however I always get an OAuthErrorEvent: silent_refresh timeout, I would expect that looking at the code in the library I would get a "Silently refreshed" OAuthSuccessEvent

Could anyone clarify why this is happening the tokens themselves seem to be refreshed so functionally there really is not an issue, it's just strange that the library throws this error.

I added some images and code (code formatting in editor seems a bit off for some reason...) of our setup, console logs and result I would expect to achieve in the library.

image

image

`import { Inject, Injectable, InjectionToken } from '@angular/core';
import { OAuthService, UrlHelperService } from 'angular-oauth2-oidc';
import { AzureB2cOptions } from '../models/azure-b2c-options';

export const AZURE_B2C_OPTIONS = new InjectionToken('AZURE_B2C_OPTIONS');

/** Authentication provider specific for Azure AD B2C */
@Injectable({
providedIn: 'root',
})
export class AzureAdB2CService {
constructor(
private oauthService: OAuthService,
private urlHelperService: UrlHelperService,
@Inject(AZURE_B2C_OPTIONS) private options: AzureB2cOptions
) {
// OAuthService - cfr. https://github.com/manfredsteyer/angular-oauth2-oidc
}

initialized = false;

public init() {
if (!this.initialized) {
this.oauthService.setupAutomaticSilentRefresh();
this.parseTokenForSignUpOrLogin();
this.initialized = true;
}

if (!this.hasValidAccessToken()) {
  const error = this.getHashParameterValue('error');
  const errorDescription = this.getHashParameterValue('error_description');

  // http://stackoverflow.com/questions/41497158/ad-b2c-self-service-password-reset-link-doesnt-work
  if (error && errorDescription && error === 'access_denied' && errorDescription.indexOf('AADB2C90118') > -1) {
    this.signUpOrLogin();
  }
}

this.oauthService.events.subscribe((e) => {
  console.log('event triggered' + JSON.stringify(e));
  console.log(localStorage.getItem('nonce'));
  console.log(localStorage.getItem('access_token'));
  console.log(localStorage.getItem('id_token'));
});

}

public signUpOrLogin() {
this.oauthService.initCodeFlow();
//this.oauthService.initImplicitFlow();
}

public logout() {
// logout for 1 policy seems to be enough, doesn't matter if it's not the "last used" policy
this.logOutPolicy();
}

/** Do we have an access token that is not expired? */
public hasValidAccessToken() {
return this.oauthService.hasValidAccessToken();
}

/** Do we have an access token, either valid or invalid? */
public hasAccessToken() {
return !!this.oauthService.getAccessToken();
}

public getAccessToken() {
return this.oauthService.getAccessToken();
}

public getIdentityClaims() {
return this.oauthService.getIdentityClaims();
}

public isAuthenticated() {
const claims = this.oauthService.getIdentityClaims();
return claims != null;
}

private parseTokenForSignUpOrLogin() {
this.initOAuthService(this.options.signupSigninPolicyname);
}

/private parseTokenForResetPassword() {
this.initOAuthService(this.options.resetPasswordPolicyname);
}
/

private getHashParameterValue(hashParameter: string) {
// const hashParts = this.oauthService.getFragment();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hashParts = this.urlHelperService.getHashFragmentParams() as any;
return hashParts[hashParameter];
}

private logOutPolicy() {
this.oauthService.logOut();
}

private initOAuthService(policyName: string) {
console.log('initOAuth', this.options, policyName);

this.oauthService.configure({
  // URL of the SPA to redirect the user to after login
  redirectUri: this.options.redirectUrl,
  // defaults to true for implicit flow and false for code flow
  // as for code code the default is using a refresh_token
  // Also see docs section 'Token Refresh'
  useSilentRefresh: true,
  // URL of the SPA to redirect the user after silent refresh
  silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html',
  timeoutFactor: 1, //For faster testing
  showDebugInformation: true,
  // The SPA's id. Register SPA with this id at the auth-server
  clientId: this.options.clientId,
  // set the scope for the permissions the client should request
  // If we want to get refresh tokens we need to add a special scope offline_access
  scope: `openid profile email https://${this.options.tenant}/api/API.Access`,
  // set to true, to receive also an id_token via OpenId Connect (OIDC) in addition to the OAuth2-based access_token
  // if oidc = true => id_token will be checked for valid audience, issuer, nonce + check for access_token hash (at_hash)
  // + token is not expired
  //oidc: true,
  responseType: 'code',
  // Issuer
  issuer:
    'https://' +
    this.options.tenant.replace('.onmicrosoft.com', '') +
    '.b2clogin.com/' +
    this.options.tenantId +
    '/v2.0/',

  tokenEndpoint:
    'https://' +
    this.options.tenant.replace('.onmicrosoft.com', '') +
    '.b2clogin.com/' +
    this.options.tenant +
    '/' +
    policyName +
    '/oauth2/v2.0/token',

  // Login url
  loginUrl:
    'https://' +
    this.options.tenant.replace('.onmicrosoft.com', '') +
    '.b2clogin.com/tfp/' +
    this.options.tenant +
    '/' +
    policyName +
    '/oauth2/v2.0/authorize',
  // Logout url
  logoutUrl:
    'https://' +
    this.options.tenant.replace('.onmicrosoft.com', '') +
    '.b2clogin.com/' +
    this.options.tenant +
    '/oauth2/v2.0/logout?p=' +
    policyName,
});

// Use setStorage to use sessionStorage or another implementation of the TS-type Storage
// instead of localStorage
// In the end we did use localStorage since Edge/IE has issues with authenticating when using sessionStorage => https://github.com/manfredsteyer/angular-oauth2-oidc/issues/390
this.oauthService.setStorage(localStorage);
 // This method just tries to parse the token(s) within the url when
// the auth-server redirects the user back to the web-app
// It doesn't send the user the the login page
// we do not need to validate our access token's signature here, our serverside API will do that for every incoming request
this.oauthService.tryLogin({}).then((response) => {
  console.log('tryLogin response: ' + response);
  console.log('has valid access token:' + this.oauthService.hasValidAccessToken());
  console.log(localStorage.getItem('access_token'));
  setTimeout(() => {
    console.log(localStorage.getItem('nonce'));
    this.oauthService
      .silentRefresh()
      .then((info) => console.debug('refresh ok', info))
      .catch((err) => console.error('refresh error', err));
    console.log('calling silent refresh with following policy: ' + this.options.signupSigninPolicyname);
  }, 20000);
});

}
}
`

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions