I’m having an infinite loop on /login/callback when my PC clock is out of sync with the server.
I get the message: “token is in the future”. I want to display a popup to inform the user that the system clock or date is incorrect. However, even after adding the popup, I can’t press the OK button to sign out the user because the login/callback is being called in a loop.
Here’s a portion of my code:
public async ngOnInit(): Promise {
const accessToken = this.oktaAuth.getAccessToken();this.isAuthenticated$ = this.oktaStateService.authState$.pipe(
filter((s: AuthState) => !!s),
map((s: AuthState) => s.isAuthenticated ?? false)
);this.router.events
.pipe(
filter((event): event is NavigationStart | NavigationEnd =>
event instanceof NavigationStart || event instanceof NavigationEnd
)
)
.subscribe(async event => {
if (event instanceof NavigationEnd) {
const pageTitle = this.getPageTitle();
this.titleService.setTitle(pageTitle);this.generalService.checkOktaClock().subscribe(clockOk => {
if (!clockOk) {
this.showClockErrorDialog = true;
}
});
}if (event instanceof NavigationStart && this.oktaAuth.isLoginRedirect()) {
try {
console.log(‘handleLoginRedirect() triggered…’);await this.oktaAuth.handleLoginRedirect(); if (accessToken) { const decodedToken = this.oktaAuth.token.decode(accessToken) as { payload: any }; this.roles = decodedToken.payload.groups; if (!Array.isArray(this.roles)) { this.roles = 'Roles not available'; } } else { this.roles = 'Token not available'; } if (currentUrl === '/' || currentUrl === '') { if (type === 4) { this.router.navigate(\['/url1'\]); } else if (type === 1) { if (this.hasRoleManager) { this.router.navigate(\['/url2'\]); } else { this.router.navigate(\[''\]); } } } } catch (err: any) { console.error('Error in handleLoginRedirect:', err); if (accessToken) { await this.oktaAuth.handleLoginRedirect(); } else { this.showClockErrorDialog = true; this.router.navigateByUrl('/login/callback', { skipLocationChange: true }); return; } // "The JWT was issued in the future" => other solution not working // if (err.message?.includes('issued in the future')) { // this.showClockErrorDialog = true; // this.router.navigateByUrl('/login/callback', { skipLocationChange: true }); // return; // } this.oktaAuth.signOut(); }}
});}
and the checkOktaClock method:
checkOktaClock(): Observable {
const url = environment.urlApi + environment.url + this.params;
const httpParams = new HttpParams();return this.httpClient.get(url, { params: httpParams, responseType: ‘text’ }).pipe(
map(serverDate => {
if (!serverDate) return true;const serverTime = new Date(serverDate).getTime();
const localTime = Date.now();
const diffMinutes = (serverTime - localTime) / 1000 / 60;return Math.abs(diffMinutes) <= 2;
}),
catchError(err => {
console.error(‘Unable to retrieve server time’, err);
return of(true);
}));
}
I’ve also noticed that even when the clock is correct, /login/callback is called twice:
-
first time with
token = undefined -
second time with a valid token.
Can someone please help me understand how to fix this infinite loop and why /login/callback gets triggered twice?
more informations:
export class LoginCallbackComponent implements OnInit {
public callbackError: any = null;
constructor(
private oktaAuthStateService: OktaAuthStateService,
@InjectInject(OKTA_AUTH) private oktaAuth: OktaAuth,
private router: Router
) {}
ngOnInit(): void {
this.handleLoginRedirect();
}
async handleLoginRedirect(): Promise {
try {
// Check if an interaction (like MFA) is required based on the current auth state
const authState = await this.oktaAuthStateService.authState$.toPromise();
if (authState?.\[‘isInteractionRequired’\]) {
// Redirect to the login page or handle interaction await this.oktaAuth.signInWithRedirect(); return;}
// Handle the login redirect and process tokens
await this.oktaAuth.handleLoginRedirect();
this.oktaAuth.start(); // Start OktaAuth services after redirect is handled
// Redirect to the home page or any other route after successful login
this.router.navigate(\[‘/’\]);
} catch (err) {
this.callbackError = err;
}
}
and
export class AuthGuard implements CanActivateChild {
roles: string = ‘’;
hasRoleManager: boolean = false;
hasRoleAdmin: boolean = false;
hasRoleUser: boolean = false;constructor(private router: Router,
@Inject(OKTA_AUTH) private oktaAuth: OktaAuth
) { }async canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Promise {
const isAuthenticated = await this.oktaAuth.isAuthenticated();
const accessToken = this.oktaAuth.getAccessToken();if (accessToken) {
const decodedToken = this.oktaAuth.token.decode(accessToken) as { payload: any };
this.roles = decodedToken.payload.groups;this.hasRoleManager = this.roles.includes(UserRole.Manager);
this.hasRoleUser = this.roles.includes(UserRole.User);
this.hasRoleAdmin = this.roles.includes(UserRole.Admin);/* Manager */
if(isAuthenticated){
if (this.hasRoleManager) {
const requestedRoute = route.routeConfig?.path;
if (requestedRoute === ‘rout1’ || requestedRoute === ‘rout2’) {
return true;
}else{
return false;
}
/* Admin & user */
} else if (!this.hasRoleManager && (this.hasRoleUser || this.hasRoleAdmin)) {
return true;
} else{
return false;
}
}else{
/* is not authenticated */
this.router.navigate([‘/login/callback’]);
return false;
}
} else{
this.router.navigate([‘/login/callback’]);
return false;
}}
}