In a previous post, I wrote about wanting to add support for Single Sign On to Serendipity and the steps I followed to launch and configure Keycloak.

There are several certified OpenID Connect (OIDC) implementations that provide OIDC and OAuth 2.0 protocal support for browser-based applications.

In this post, I thought I’d take a look at oidc-client-js.

Install oidc-client

I installed oidc-client using npm:

npm install -P oidc-client

Create an Auth Library

I used the Angular CLI to generate the scaffolding for a new library:

ng generate library auth-oidc --prefix=auth

Create an Auth Service

I generated the scaffolding for a new service:

ng generate service services/auth/auth --project=auth-oidc

I updated the Auth service as follows:

import { Inject, Injectable } from '@angular/core';
import { Router } from '@angular/router';

import { BehaviorSubject } from 'rxjs';

import { UserManager, UserManagerSettings, User } from 'oidc-client';

import { OidcConfig } from '../../models/models';
import { OidcConfigService } from '../config.service';

import { Auth } from 'auth';

import { LoggerService } from 'utils';

@Injectable({
  providedIn: 'root'
})
export class OidcAuthService extends Auth {

  private authState$ = new BehaviorSubject(false);

  private authService: UserManager;
  private user: User = null;

  constructor(@Inject(OidcConfigService) private config: OidcConfig,
              private router: Router,
              private logger: LoggerService) {

    super();

    const oidcConfig: UserManagerSettings = {
      authority: this.config.oidc.issuer,
      client_id: this.config.oidc.clientId,
      redirect_uri: this.config.oidc.redirectUri,
      post_logout_redirect_uri: this.config.oidc.postLogoutRedirectUri,
      response_type: this.config.oidc.responseType,
      scope: this.config.oidc.scope,
      filterProtocolClaims: this.config.oidc.filterProtocolClaims,
      loadUserInfo: this.config.oidc.loadUserInfo
    };

    this.authService = new UserManager(oidcConfig);

    this._isAuthenticated().then(state => {

      this.authState$.next(state);

      this.authState$.subscribe((authenticated: boolean) => {

        this.authenticated = authenticated;
        this.accessToken = '';

        if (this.authenticated) {
          this.setAccessToken();
        }

      });

    });

  }

  public isAuthenticated(): boolean {
    return this.authenticated;
  }

  public getAccessToken(): string {
    return this.accessToken;
  }

  public getIdToken(): string {
    return this.idToken;
  }

  private setAccessToken() {
    this.accessToken = this.user.access_token;
  }

  public async loginWithRedirect(): Promise<void> {
    return this.authService.signinRedirect();
  }

  public async handleRedirectCallback(): Promise<void> {

    this.user = await this.authService.signinRedirectCallback();

    this.authenticated = await this._isAuthenticated();

    this.authState$.next(this.authenticated);

    this.router.navigate(['/']);
  }

  public logout(returnUrl: string) {

    this.authState$.next(false);

    this.authService.signoutRedirect();
  }

  //
  // Private methods
  //

  private async _isAuthenticated(): Promise<boolean> {
    return this.user !== null && !this.user.expired;
  }

}

Create a Route Guard

I generated the scaffolding for a new route guard:

ng generate guard guards/auth/auth --project=auth-oidc

I updated the Auth guard as follows:

...

@Injectable({
  providedIn: 'root'
})
export class OidcAuthGuard implements CanActivate {

  constructor(private router: Router,
              private authService: AuthService) {}

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

    if (this.authService.isAuthenticated()) {
      return true;
    }

    this.router.navigate(['/login'], { queryParams: { returnUrl: state.url }});

    return false;
  }

}

Create a Login Redirect Component

I used the Angular CLI to generate the scaffolding for a new component:

ng generate component components/login-redirect --project=auth-oidc

I updated the Login Redirect component as follows:

...

@Component({
  template: ``,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoginRedirectComponent implements OnInit {

  constructor(private authService: AuthService,
              private logger: LoggerService) {
  }

  ngOnInit() {
    this.authService.loginWithRedirect();
  }

}

Create an Authorization Code Callback Component

I generated the scaffolding for a new component:

ng generate component components/authorization-code-callback --project=auth-oidc

I updated the Authorization Code Callback component as follows:

...

@Component({
  template: ``,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AuthorizationCodeCallbackComponent implements OnInit {

  constructor(private authService: AuthService,
              private logger: LoggerService) {
  }

  ngOnInit() {
    this.authService.handleRedirectCallback();
  }

}

Update the Auth Library’s Routing Module

I updated the Auth Library’s Routing module as follows:

...

const routes: Routes = [
  {
    path: 'login',
    component: LoginRedirectComponent
  },
  {
    path: 'authorization-code/callback',
    component: AuthorizationCodeCallbackComponent
  }
];

@NgModule({
  imports: [ RouterModule.forChild(routes)],
  exports: [ RouterModule ]
})
export class LibRoutingModule {}

Update Serendipity’s App Module

I updated Seredipity’s App module as follows:

...

// import { LocalAuthModule, authProviders } from 'auth-local';
import { OidcAuthModule, authProviders } from 'auth-oidc';

@NgModule({
  imports: [
    BrowserModule,
    // LocalAuthModule,
    OidcAuthModule.forRoot(environment),
    CoreModule,
    AppRoutingModule
  ],
  declarations: [ AppComponent ],
  providers: [
    loggerProviders,
    authProviders,
    angularMaterialProviders,
    {
      provide: ErrorHandler,
      useClass: GlobalErrorHandler
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: HttpErrorInterceptor,
      multi: true
    }
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

Now when a user launches Serendipity they will be redirected to Keycloak:

And then directed back to the application:

Source Code:
References:



Source link