Description
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.
`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);
});
}
}
`