Token is in the future with infinite loop /login/callback

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;
}

}
}

Hi there @sabrinaBinaa891 , welcome to the community!

Sorry to hear you’re running into trouble. Let’s troubleshoot and figure this out together.

One thing I notices is you’re setting the access token in the OnInit method outside of the router event subscription. At this point, it’s expected that the token is undefined - it will only be set after the handleLoginRedirect() method completion.

Can you change the accessToken variable declaration to let accessToken = this.oktaAuth.getAccessToken(), and re-set the accessToken after the handleLoginRedirect()?

Hopefully that removes one /login/callback redirect, then we can troubleshoot the time-handling next. :slight_smile:

Let us know how things change with this first change.

I moved this.oktaAuth.getAccessToken(), but the infinite loop issue on /callback/login still persists, even when I used an alert to block the redirection. As soon as I click OK on the alert, I get redirected to login/callback. Here’s my modified code in app.component.ts

public async ngOnInit(): Promise {

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
),
takeUntil(this.destroySub$)
)
.subscribe(async event => {
if (event instanceof NavigationEnd) {
const pageTitle = this.getPageTitle();
this.titleService.setTitle(pageTitle);
}

if (event instanceof NavigationStart && this.oktaAuth.isLoginRedirect()) {

  try {
    await this.oktaAuth.handleLoginRedirect();
    const accessToken = this.oktaAuth.getAccessToken();
    if (accessToken) {
      const decodedToken = this.oktaAuth.token.decode(accessToken) as { payload: any };
      this.roles = decodedToken.payload.groups;
      if (Array.isArray(this.roles)) {
        this.hasRoleManager =.....;
        this.hasRoleUser = .....;
        this.hasRoleAdmin = .....;
      } else {
        this.roles = 'Roles not available';
      }
    } else {
      this.roles = 'Token not available';
    }
    const type = this.generalService.getType();
    const currentUrl = this.router.url;

    if (currentUrl === '/' || currentUrl === '') {
      if (type === 4) {
        this.router.navigate(\['/rout1'\]);
      } else if (type === 1) {
        if (this.hasRoleManager) {
          this.router.navigate(\['/rout2'\]);
        } else {
          this.router.navigate(\[''\]);
        }
      }
    }


} catch (err: any) {
console.error(‘Erreur handleLoginRedirect:’, err);
// “The JWT was issued in the future”
if (err.message?.includes(‘issued in the future’)) {
window.alert(

          "OKTA Authentication error\\n\\n" +
          "Unable to validate your access via OKTA. " +
          "Please synchronize your time zone before trying again!"
        
      )
      await this.oktaAuth.signOut();
    }
  }

}

});

}

Are you still seeing the looping even when the issue time isn’t in the future?

Yes, unfortunately, the issue still occurs even when the issue time is not in the future.
It’s not exactly a loop in that case — the redirection to /login/callback just happens twice. However, when the issue time is in the future, it turns into a continuous loop.

Can you put together a small project reproducing the issue?

It might be easier than trying to troubleshoot over the forum.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.