Search code examples
angularoauth-2.0keycloakangular-oauth2-oidc

OAuth2 code flow with angular-oauth2-oidc and Keycloak


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.

I'm able to redirect a user from my "home" page to the "login" page of Keycloak on a button click, and normally, I would redirect the user to the "landing" page of my application after successful login. However, when I do that, I realized the access token is not yet set to my browser storage when Keycloak redirects the user to the "landing" page. So instead, I had to redirect the user back to "home" page instead, and then to the "landing" page, so that in the mean time tokens are set to storage.

Obviously, this is not a good practice and I believe I'm doing something wrong there. Here are the codes that I've been working on;

home.component.html

<button class="btn btn-default" (click)="login()">
  Login
</button>

home.component.ts

login(): void {
   this.authService.login();
}

auth.service.ts

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

  constructor(private oauthService: OAuthService, private router: Router) {
    this.configure();
  }

  authConfig: AuthConfig = {
    issuer: ...
    redirectUri: window.location.origin + '/home',
    clientId: ...
    scope: ...
    responseType: 'code',
    disableAtHashCheck: true
  }

  login(): {
    this.oauthService.initCodeFlow();
  }

  private configure() {
    this.oauthService.configure(this.authConfig);
    this.oauthService.tokenValidationHandler = new JwksValidationHandler();
    this.oauthService.loadDiscoveryDocumentAndTryLogin().then(() => {
      if(this.hasValidToken()){
        this.oauthService.setupAutomaticSilentRefresh();
        this.router.navigateByUrl('/landing');
      }
    });
  }
}

What I want to do instead would be something like this;

auth.service.ts.

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

  constructor(private oauthService: OAuthService, private router: Router) {
    this.configure();
  }

  authConfig: AuthConfig = {
    issuer: ...
    redirectUri: window.location.origin + '/landing',
    clientId: ...
    scope: ...
    responseType: 'code',
    disableAtHashCheck: true
  }

  login(): {
    this.oauthService.initCodeFlow();
  }

  private configure() {
    this.oauthService.configure(this.authConfig);
    this.oauthService.tokenValidationHandler = new JwksValidationHandler();
    this.oauthService.loadDiscoveryDocumentAndTryLogin().then(() => {
      if(this.hasValidToken()){
        this.oauthService.setupAutomaticSilentRefresh();
      }
    });
  }
}

Any help would be appreciated.


Solution

  • The OAuth2/OpenID process is often inherently async, for example loading the discovery document can take some 100s of milliseconds. Since a while now, Angular has async app initializers: if you return a promise from an initializer Angular will await it until it further goes on to do routing and whatnot.

    You can have a look at my sample implementation and even try to clone it and reconfigure it against your Keycloak. The important bits are as follows.

    An async app initializer like this:

    export function authAppInitializerFactory(authService: AuthService): () => Promise<void> {
      return () => authService.runInitialLoginSequence();
    }
    

    The runInitialLoginSequence() method does something similar to your configure() method, the main differences being:

    • it returns a promise (your method returns immediately, without awaiting the promise) and makes Angular wait for it to resolve
    • it has logic around the state parameter, OIDC's method of ensuring the user goes to the page they intended to go to before being asked to log in

    I recommend checking out that login sequence method for inspiration. (Note that I don't call .configure(...) on the library's service, because I prefer to register everything in the module as providers.)