angular-oauth2-oidc: Tokens are not set immediately after redirect

Hello,

I am using version 3.1.4 of this library. My set-up is as follows:
after users enters his credentials on the identity server he is redirected to a protected resource.
My canActivate method looks as follows.

canActivate(): boolean {
    const validIdToken = this.oauthService.hasValidIdToken;
    const validAccessToken = this.oauthService.hasValidAccessToken();
    return (validIdToken && validAccessToken);
  }

However, at the time when canActivate() is called both tokens are not available immediately
(even though user is authenticated and they should be set).
canActivate() also returns false. I can see they eventually arrive:

this.oauthService.events.subscribe(({ type } : OAuthEvent) => {
      switch (type) {
        case 'token_received':
          const idToken = this.oauthService.getIdToken();
          const accessToken = this.oauthService.getAccessToken();
          if (accessToken && idToken) {
            console.log(accessToken);
            console.log(idToken);
          }
      }
 });

Is there some way to prevent this - ensuring that they are already set when canActivate() is called?

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 2
  • Comments: 16 (5 by maintainers)

Most upvoted comments

@Razzeee In my app, on routing i use CanActivate - which secure access to component before you are not login ( You are not logged - component cannot be load, first login bro!)

My app.component.ts

export class AppComponent {

    constructor(private oauthService: OAuthService) {
        this.oauthService.configure(authConfig);
        // this.oauthService.setStorage(localStorage);
        this.oauthService.tokenValidationHandler = new JwksValidationHandler();
        this.oauthService.setupAutomaticSilentRefresh();

        this.oauthService.loadDiscoveryDocumentAndLogin();
    }


Routing:


export const routes: Routes = [
    {
        path: '',
        redirectTo: 'dashboard',
        pathMatch: 'full'
    },
    {
        path: '',
        canActivate: [AuthGuard], //guard secure acces
        component: FullLayoutComponent,
        data: {
            title: 'Home'
        },
        children: [
            {
                path: 'dashboard',
                loadChildren: './dashboard/dashboard.module#DashboardModule'
            },
            {
                path: 'UsersList',
                loadChildren: './users/users-list/users-list.module#UsersListModule'
            },
            // ,
            // { path: '**', redirectTo: 'dashboard' }
        ],
    },
    // { path: '**', redirectTo: 'dashboard' },
];

And finally guard


@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private oauthService: OAuthService, private router: Router) {}

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
        return this.oauthService
            .loadDiscoveryDocumentAndTryLogin()
            .then((res) => {
                return this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken()
            });
    }
}

you’ll need to resolve the above event that you demonstrate subscribing to within your guard because you must wait for the discovery document to load, which is async. canActivate can accept a promise return, or better yet an Observable<boolean>. One option might be to use the OAuthService.TryLogin() which returns a promise, something like:

return this.oauthService
      .tryLogin()
      .then(() => { this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken() }

*The above is pseudo-code, your implementation will most likely vary. HTH

@Razzeee In my app, on routing i use CanActivate - which secure access to component before you are not login ( You are not logged - component cannot be load, first login bro!)

My app.component.ts

export class AppComponent {

    constructor(private oauthService: OAuthService) {
        this.oauthService.configure(authConfig);
        // this.oauthService.setStorage(localStorage);
        this.oauthService.tokenValidationHandler = new JwksValidationHandler();
        this.oauthService.setupAutomaticSilentRefresh();

        this.oauthService.loadDiscoveryDocumentAndLogin();
    }

Routing:


export const routes: Routes = [
    {
        path: '',
        redirectTo: 'dashboard',
        pathMatch: 'full'
    },
    {
        path: '',
        canActivate: [AuthGuard], //guard secure acces
        component: FullLayoutComponent,
        data: {
            title: 'Home'
        },
        children: [
            {
                path: 'dashboard',
                loadChildren: './dashboard/dashboard.module#DashboardModule'
            },
            {
                path: 'UsersList',
                loadChildren: './users/users-list/users-list.module#UsersListModule'
            },
            // ,
            // { path: '**', redirectTo: 'dashboard' }
        ],
    },
    // { path: '**', redirectTo: 'dashboard' },
];

And finally guard


@Injectable()
export class AuthGuard implements CanActivate {

    constructor(private oauthService: OAuthService, private router: Router) {}

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
        return this.oauthService
            .loadDiscoveryDocumentAndTryLogin()
            .then((res) => {
                return this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken()
            });
    }
}

谢谢你朋友,我通过你的方法解决了,页面登录死循环问题,很感谢你! 这是我app.component.ts的代码 import { Component, OnInit } from '@angular/core'; import { OAuthService, JwksValidationHandler } from 'angular-oauth2-oidc'; import { } from 'angular-oauth2-oidc'; import { authConfig } from './auth.config'; import { Router } from '@angular/router'; import { Observable } from 'rxjs'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.less'] }) export class AppComponent implements OnInit { constructor(private authService: OAuthService, public router: Router, public routerActive: ActivatedRoute) { this.authService.configure(authConfig); this.authService.tokenValidationHandler = new JwksValidationHandler(); this.authService.loadDiscoveryDocumentAndLogin(); } ngOnInit() { } } 这是auth.guard.ts `import { Injectable } from ‘@angular/core’; import {CanActivate, Router } from ‘@angular/router’; import { OAuthService, JwksValidationHandler } from ‘angular-oauth2-oidc’; import { authConfig } from ‘…/…/auth.config’; import { Observable } from ‘rxjs’;

@Injectable() export class AuthGuard implements CanActivate {

constructor(private oauthService: OAuthService, private router: Router) { }

canActivate(): Promise<boolean> {
    return this.oauthService
        .loadDiscoveryDocumentAndTryLogin()
        .then((res) => {
            return this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken()
        });
}

}`

I have a similar approach as OP mentioned above with an async CanActivate guard (returning an async boolean). However, I factored that asyncness into a wrapper for this library’s OAuthService. You can check out the repository with a demo. Here’s a relevant snippet:

@Injectable({ providedIn: 'root' })
export class AuthService {

  private isAuthenticatedSubject$ = new BehaviorSubject<boolean>(false);
  public isAuthenticated$ = this.isAuthenticatedSubject$.asObservable();

  private isDoneLoadingSubject$ = new ReplaySubject<boolean>();
  public isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();

  public canActivateProtectedRoutes$: Observable<boolean> = combineLatest(
    this.isAuthenticated$,
    this.isDoneLoading$
  ).pipe(map(values => values.every(b => b)));
  
  // ... etc.

The guard is really basic:

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
  ) { }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
  ): Observable<boolean> {
    return this.authService.canActivateProtectedRoutes$
      .pipe(tap(x => console.log('You tried to go to ' + state.url + ' and this guard said ' + x)));
  }
}

The guard just relies on this service. So just like OP’s comment you can prevent components from loading before auth loading is done.

If you’re still getting API calls going out before auth is done, you will have to make them async as well, and rely on the auth service’s async booleans. For example, if you try to do a call in application startup, or in another core service, you have to make those calls “wait” until authService.isDoneLoading$ publishes a truthy value. For example:

// Untested!
@Injectable()
export class SomeCoreService {
  constructor(authService: OAuthService, http: HttpClient) {
    // Do some loading here already, but wait for auth to be ready!
    authService.isDoneLoading$.subscribe(_ => {
      http.get<Foo>('https://example.org/api/foo')
        .subscribe(foo => console.log('Foo ready!', foo));
    });
  }  
}

And then you won’t have any calls go out anymore before the authorization is done loading (or before you’re authorized, depending on whether you rely on isDoneLoading$ or canActivateProtectedRoutes$).

As a last resort, if you have a really weird edge case, I suppose you could also have an Interceptor that delays all calls to APIs until the auth service is done loading. But I haven’t found a need for that in my production applications at all.

Thank you so much for your detailed reply. I really appreciate it. It helps a lot.

This worked for me:

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {

    if (this.oauthService.hasValidIdToken()) {
      return Promise.resolve(true);
    }

    return this.oauthService.loadDiscoveryDocumentAndTryLogin()
      .then(_ => {
        return this.oauthService.hasValidIdToken() && this.oauthService.hasValidAccessToken();
      })
      .then(valid => {
        if (!valid) {
          this.router.navigate(['/unauthorized']);
        }
        return valid;
      });
  }

I got the same problem, but it’s a bit different for me. As I’m using the api interceptor to add the bearer. But the first requests after being logged in fails due to no auth header, seems like the api calls are send faster then the interceptor knows of the token.