Description
Describe the bug
this.oauthService.setupAutomaticSilentRefresh();
goes into infinite loop after access token expires.
Using the code+pkce flow without the offline_access
scope.
It is unclear that offline_access
is required as per this issue and is unavailable to me because of my auth provider (pingfed).
It refreshes, gets a new valid access, id, and refresh token, and then refreshes again.
My auth server is configured for very short access_token life spans. They're configured for 5 minutes.
From my understanding refresh tokens will still work without the offline_access scope as long as the refresh happens before the expiration of the access_token.
Using version "angular-oauth2-oidc": "^12.1.0"
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { OAuthErrorEvent, OAuthService } from 'angular-oauth2-oidc';
@Injectable({ providedIn: 'root' })
export class AuthService {
private isAuthenticatedSubject$ = new BehaviorSubject<boolean>(false);
isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();
private isDoneLoadingSubject$ = new BehaviorSubject<boolean>(false);
isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();
/**
* Publishes `true` if and only if (a) all the asynchronous initial
* login calls have completed or errorred, and (b) the user ended up
* being authenticated.
*
* In essence, it combines:
*
* - the latest known state of whether the user is authorized
* - whether the ajax calls for initial log in have all been done
*/
canActivateProtectedRoutes$: Observable<boolean> = combineLatest([
this.isAuthenticated$,
this.isDoneLoading$,
]).pipe(map((values) => values.every((b) => b)));
constructor(private oauthService: OAuthService, private router: Router) {
// Useful for debugging:
this.oauthService.events.subscribe((event) => {
if (event instanceof OAuthErrorEvent) {
console.error('OAuthErrorEvent Object:', event);
} else {
console.warn('OAuthEvent Object:', event);
}
});
// This is tricky, as it might cause race conditions (where access_token is set in another
// tab before everything is said and done there.
// TODO: Improve this setup. See: https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/issues/2
window.addEventListener('storage', (event) => {
// The `key` is `null` if the event was caused by `storage.clear()`
if (
(event.key !== 'access_token' && event.key !== null) ||
event.key === null
) {
return;
}
console.warn(
'Noticed changes to access_token (most likely from another tab), updating isAuthenticated'
);
this.isAuthenticatedSubject$.next(
this.oauthService.hasValidAccessToken()
);
if (!this.oauthService.hasValidAccessToken()) {
this.navigateToLoginPage();
}
});
this.oauthService.events.subscribe(() => {
this.isAuthenticatedSubject$.next(
this.oauthService.hasValidAccessToken()
);
});
this.isAuthenticatedSubject$.next(this.oauthService.hasValidAccessToken());
this.oauthService.events
.pipe(filter((e) => ['token_received'].includes(e.type)))
.subscribe(() => this.oauthService.loadUserProfile());
this.oauthService.events
.pipe(
filter((e) => ['session_terminated', 'session_error'].includes(e.type))
)
.subscribe(() => this.navigateToLoginPage());
this.oauthService.setupAutomaticSilentRefresh(); // Commenting this out eliminates the loop but does not provide refresh functionality.
}
private navigateToLoginPage() {
this.router.navigateByUrl('/login');
}
runInitialLoginSequence(): Promise<void> {
if (location.hash) {
console.log('Encountered hash fragment, plotting as table...');
console.table(
location.hash
.substr(1)
.split('&')
.map((kvp) => kvp.split('='))
);
}
return this.oauthService
.loadDiscoveryDocument()
.then(() => this.oauthService.tryLogin())
.then(() => {
this.isDoneLoadingSubject$.next(true);
})
.catch(() => this.isDoneLoadingSubject$.next(true));
}
login(targetUrl?: string) {
this.oauthService.initLoginFlow(targetUrl || this.router.url);
}
logout() {
this.oauthService.logOut();
}
hasValidToken() {
return this.oauthService.hasValidAccessToken();
}
// These normally won't be exposed from a service like this, but
// for debugging it makes sense.
get accessToken() {
return this.oauthService.getAccessToken();
}
get refreshToken() {
return this.oauthService.getRefreshToken();
}
get identityClaims() {
return this.oauthService.getIdentityClaims();
}
get idToken() {
return this.oauthService.getIdToken();
}
get logoutUrl() {
return this.oauthService.logoutUrl;
}
}
Expected behavior
Silent Refresh should refresh the token and not cause an infinite loop.
Desktop (please complete the following information):
- OS: MacOs
- Browser: Chrome
- Version: 112.0.5615.137
Additional context
I based this off of one of the examples and stripped all but code + pkce functionality.
"angular-oauth2-oidc": "^12.1.0"