Oidc-client-js: [Help wanted] Angular2 - access_token not available immediately after login

Created on 12 Jul 2017  路  8Comments  路  Source: IdentityModel/oidc-client-js

I have an application in Angular4 that shall not be accessable for non signed in users. I'm using Angular Guards to check for the logged in state and redirecting to signin if necessary.

canActivate(): boolean {
        let isLoggedIn = this._authService.isLoggedIn;
        isLoggedIn.subscribe((loggedin) => {
            if (!loggedin) {
                this._authService.manager.signinRedirect();
            }
        });
        return isLoggedIn.getValue();
    }

After redirecting I'm initalizing some http calls that needs the access_token.
But those first initial calls cant find the access_token.

How can I "postpone" the bootstraping of the application until OIDC-CLIENT is truly finished loading and have the User model ready for all Angular classes?

image

  1. The user object promise is initialized which basically is oidc-client.UserManager.getUser()
  2. The HTTP request is executed - but the oidc-client library is not yet ready so it does not have access to the User.access_token. Asking the oidc-client if user is logged in returns false.
this._authService.manager.getUser().then((user) => {
            if (user) {
                this.loggedIn = true;
                this.currentUser = user;
                this.userLoadededEvent.emit(user);
            } else {
                this.loggedIn = false;
            }
        })
  1. Since access_token is not in the header, a 403 is returned from the API.
  2. The redirect component is initialized:
ngOnInit() {
        this._authService.manager.signinRedirectCallback().then(
            () => {
                console.log('SigninOidcComponent : signinRedirectCallback');
                this._router.navigate(['']);
            });
    }

If I now navigate the application, the http-service is able to find the access_token:
image

question

Most helpful comment

@jonasbg In my sample you should be subscribing to the Observable in the auth.service before you make any calls that require authentication. You can inject the auth.service into the AppComponent and do that in the constructor.

constructor(authService: AuthService, userService: UserService) {
     authService.authenticationChallenge$.subscribe((isAuthenticated)=> {
         if(isAuthenticated) {
              // call your user service here
         }
    }
}

Also, as a note - you shouldn't be accessing the manager in the auth service directly like you are doing in your AuthGuardService. It violates the Law of Demeter. The manager should be marked as private and the API for the class should expose a login method.

@tibbus IMHO APP_INITIALIZER is not the proper place to do authentication. In an Angular app you should be protecting routes like you protect endpoints in a Web Api. The APP_INITILAIZER should be user for things like loading configuration. Using a route guard on the base route makes the code easier to understand and makes reconfiguring the app easier if you need to have a route that is unprotected in the future. Also, the APP_INITIALIZER initializes all of the routing in the application so whether you user APP_INITIALIZER or a guard on the base route you are doing the same thing. But, like I said that is just my opinion.

@SebastianStehle I took a look at the code in your Squidex project. Nice work. I can learn a few things from it.

My sample code started out as just a sample of configuring Identity Server and an Angular Client. I am going to do some more work on it to make it a complete sample.

All 8 comments

You need to use the the APP_INITIALIZER

This how it looks in my app :

app.module.ts

import { APP_INITIALIZER } from '@angular/core';
import { AuthInitializer } from './services/auth/authInitializer';
    ...
    providers: [
        ...
        {
            provide: APP_INITIALIZER,
            useFactory: AuthInitializer,
            deps: [AuthService],
            multi: true
        }
    ],

authInitializer.ts:

import { AuthService } from './auth.service';

export function AuthInitializer(auth: AuthService) { return () => auth.setUser() };

auth.service.ts :

    ...
    public setUser() {
        return new Promise(resolve => {
            this.mgr.getUser().then(authData => {
              ....
              resolve(true);
            });
        });
    }

You just have to return a promise or Observable in your Guard.

Like @SebastianStehle says you need to return an Observable or Promise from your guard. I have a complete working sample of an Angular client authenticating against IdentityServer4 at start up in my repo https://github.com/andrewalderson/angular-identityserver. Just created this over the weekend so no docs yet :(

@andrewalderson I had a look to your sample. You could get an expired access token with getUser(), right?

At the moment I do a signinSilent, because I don't trust my own server. A proper solution would be to make a retry in the interceptor. I should try it out.

Btw: this is my interceptor:

https://github.com/Squidex/squidex/blob/master/src/Squidex/app/shared/interceptors/auth.interceptor.ts

And my auth service:

https://github.com/Squidex/squidex/blob/master/src/Squidex/app/shared/services/auth.service.ts

@andrewalderson thank you. I've tried to follow the GIT code, and I've both implemented your code into my solution and tried to implement my code into yours. The error I'm getting is: ERROR TypeError: Cannot read property 'access_token' of undefined which is because the authService exists before the accessToken is available,

if (this.authService) {
            if (!options.headers) {
                options.headers = new Headers();
            }
            options.headers.append('Authorization', 'Bearer ' + this.authService.accessToken);
        }

It works when I have the dataService call inside home.module, but when I take the dataService call outside and into app.module it can't get the access_token.

@tibbus I've tried this solution as well. But the HTTP request is executed before the user is returned from the Oidc-library.

@SebastianStehle tried that. This is my AuthGuardService canActive:

canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
        let isLoggedIn = this._http.isLoggedInObs();
        isLoggedIn.subscribe((loggedin) => {
            if (!loggedin) {
                this._authService.manager.signinRedirect();
            }
        });
        return isLoggedIn;
    }

After working with the GIT code from @andrewalderson I think that some of the issue in my scenario is that I am trying to execute HTTP request (access user information) from the outermost module (app.module).

What I am trying to achieve is to send an authenticated http request to get extended user information (with token from the JWT) from an web-api service (.NET) and present that information into a ribbon/navbar.

I've gotten my solution to work when I refresh my tab, but when I open a new tab and navigate to the solution, which invokes a redirect to the STS and back I cannot get User information fast enough.

With the APP_INITIALIZER nothing should be called before, my app waits until it gets a response from the identity server getUser().then... promise .

It checks what is returned from getUser() if everything is ok I just rezolve the promise for the initializer and then Angular starts the app. (it doesn't even call the providers constructor before the initializer promise is resolved)

@jonasbg In my sample you should be subscribing to the Observable in the auth.service before you make any calls that require authentication. You can inject the auth.service into the AppComponent and do that in the constructor.

constructor(authService: AuthService, userService: UserService) {
     authService.authenticationChallenge$.subscribe((isAuthenticated)=> {
         if(isAuthenticated) {
              // call your user service here
         }
    }
}

Also, as a note - you shouldn't be accessing the manager in the auth service directly like you are doing in your AuthGuardService. It violates the Law of Demeter. The manager should be marked as private and the API for the class should expose a login method.

@tibbus IMHO APP_INITIALIZER is not the proper place to do authentication. In an Angular app you should be protecting routes like you protect endpoints in a Web Api. The APP_INITILAIZER should be user for things like loading configuration. Using a route guard on the base route makes the code easier to understand and makes reconfiguring the app easier if you need to have a route that is unprotected in the future. Also, the APP_INITIALIZER initializes all of the routing in the application so whether you user APP_INITIALIZER or a guard on the base route you are doing the same thing. But, like I said that is just my opinion.

@SebastianStehle I took a look at the code in your Squidex project. Nice work. I can learn a few things from it.

My sample code started out as just a sample of configuring Identity Server and an Angular Client. I am going to do some more work on it to make it a complete sample.

All set on this issue? Can we close?

Was this page helpful?
0 / 5 - 0 ratings