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

Created on 23 Jan 2018  ·  16Comments  ·  Source: manfredsteyer/angular-oauth2-oidc

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?

Most helpful comment

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. 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

All 16 comments

I have this issue too

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. 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

@EricWafford Exacly, that solve my issue :)

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.

@Razzeee But you call api before activate website? This smells like bad pratice.

@meron1122 I don't think I do, well at least not intended.
app.component.ts does the typical constructor stuff

        this.oauthService.configure(authConfig);
        this.oauthService.tokenValidationHandler = new JwksValidationHandler();
        this.oauthService.loadDiscoveryDocumentAndLogin();

The last line will redirect us to the activate website if we're not logged in. So nothing else should happen hopefully. If we enter correct credentials and get redirected a sub component will call an api on ngOnInit. That one does end up without headers in that case. If I refresh via F5 everything is fine and gets called correctly.

@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()
            });
    }
}

@meron1122
Thanks for the kind reply. While that fixes the glitch that unauthed users see the page their trying to access for a second. It still happens that unauthorized calls without headers are send to the backend after login redirect.

@Razzeee you can make a PoC - plunker of something?

I probably can, but will take me some time.

I omited this.oauthService.setupAutomaticSilentRefresh(); from your example code, but that can't be the problem right?

I fixed this race condition issue with this patch for my code https://github.com/xmlking/nx-starter-kit/commit/b6f735f34cd3350f27315133d4e104cd0769c003

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

@meron1122
Thanks for the kind reply. While that fixes the glitch that unauthed users see the page their trying to access for a second. It still happens that unauthorized calls without headers are send to the backend after login redirect.

@meron1122 I don't think I do, well at least not intended.
app.component.ts does the typical constructor stuff

        this.oauthService.configure(authConfig);
        this.oauthService.tokenValidationHandler = new JwksValidationHandler();
        this.oauthService.loadDiscoveryDocumentAndLogin();

The last line will redirect us to the activate website if we're not logged in. So nothing else should happen hopefully. If we enter correct credentials and get redirected a sub component will call an api on ngOnInit. That one does end up without headers in that case. If I refresh via F5 everything is fine and gets called correctly.

I am facing the same problem like you. Can you please let me know how did you fix the issue? Thank you so much in advance.

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.

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.

@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()
        });
}

}`

Was this page helpful?
0 / 5 - 0 ratings