0

I'm trying to integrate with an identity provider (Keycloak in my case) in my Angular project. I'm using "angular-oauth2-oidc" library for that purpose.

So, I'm able to redirect a user from my home page to the login page of IDP on a button click, and it redirects me back to my original page after a successful login. So far so good, but my problem is that after the login process, session information including token is not set to my browser's session storage. If I repeat the process (calling the login function again), it then sets them correctly.

Here are the codes that I've worked on so far;

auth.service.ts

  constructor(private oauthService: OAuthService) {}

  authConfig: AuthConfig = {
    issuer: environment.keycloak.issuerAddress,
    redirectUri: window.location.origin + '/home',
    clientId: environment.keycloak.clientId,
    scope: environment.keycloak.scope,
    responseType: environment.keycloak.responseType,
    disableAtHashCheck: environment.keycloak.disableAtHashCheck,
    showDebugInformation: environment.keycloak.showDebugInformation,
  }

  login(): Promise<any> {
    return new Promise<void>((resolveFn, rejectFn) => {
      this.initLogin().then(() => {
        resolveFn();
      }).catch(function(e){
        rejectFn(e);
      });
    });
  }

  private initLogin(): Promise<any> {
    return new Promise<void>((resolveFn, rejectFn) => {
      this.oauthService.configure(this.authConfig);
      this.oauthService.tokenValidationHandler = new JwksValidationHandler();
      this.oauthService.loadDiscoveryDocumentAndTryLogin().then(() => {
        if (this.oauthService.hasValidAccessToken()) {
          this.oauthService.setupAutomaticSilentRefresh();
          resolveFn();
        }else {
          this.oauthService.initCodeFlow();
          resolveFn();
        }
      }).catch(function(e){
        rejectFn("Identity Provider is not reachable!");
      });
    });
  }

home.component.ts

 login(): void {
    this.authService.login().then(() => {
      //
    }).catch((e) =>{
      //
    });
 }

In summary, what I'm trying to do is that;

  • When the user clicks on login button, configure the oauthService and try to login.
  • If there's already a valid access token, then just setup a silent refresh and return.
  • If there's no valid access token, then start the code flow and redirect to IDP's login page.
  • If login try is failed with an exception, tell the user that IDP is not available.

Note: If I instead do the oauthService configuration in the constructor, and only call the oauthService.initCodeFlow() method when the user wants to login, then it works fine. The reason I'm not configuring it in constructor is that I want to be able to tell the user that IDP is not available when the user clicks on login button.

josh
  • 409
  • 1
  • 5
  • 18

1 Answers1

0

I find it cleanest to create a guard that enforces login. You can then now decide to put that guard on all your routes, but it also allows you later to make exceptions to that. For example an FAQ page in your app might be publicly available? To duplicate said guard's code here:

canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot,
): Observable<boolean> {
    return this.authService.isDoneLoading$.pipe(
      filter(isDone => isDone),
      switchMap(_ => this.authService.isAuthenticated$),
      tap(isAuthenticated => isAuthenticated || this.authService.login(state.url)),
    );
}

It will wait for authorization logic to be isDoneLoading$ just to make sure all async authorization bootstrapping is done. It then checks if the user is authenticated, and if not sends the user off to log in, but remembers the targeted page in the parameter to login(...). That url will be provided back to you when the user returns to your application.

In the login sequence of your app (here's my sample) you can read this state and use it to send the user to the originally intended page. The guard should now allow this, because the user has since signed in.

Here's the relevant part of the login sequence to do this:

    // Check for the strings 'undefined' and 'null' just to be sure. Our current
    // login(...) should never have this, but in case someone ever calls
    // initImplicitFlow(undefined | null) this could happen.
    if (this.oauthService.state && this.oauthService.state !== 'undefined' && this.oauthService.state !== 'null') {
      let stateUrl = this.oauthService.state;
      if (stateUrl.startsWith('/') === false) {
        stateUrl = decodeURIComponent(stateUrl);
      }
      console.log(`There was state of ${this.oauthService.state}, so we are sending you to: ${stateUrl}`);
      this.router.navigateByUrl(stateUrl);
    }
Jeroen
  • 60,696
  • 40
  • 206
  • 339