Angular

How to integrate Single Sign-on with Angular and Java Spring 5 frameworks

The following example illustrates how SSO can be integrated within your applications' code. The example shows how would an application using Angular enable the SSO feature via OpenID-Connect.

Angular Guide for SSO OpenID-Connect

This Angular guide requires the following dependency via npm:

  • angular-oauth2-oidc

Run in terminal:

npm install angular-oauth2-oidc --save

Add an environment.ts file to your project directory. This will contain the required credentials to connect to IndustryApps Keycloak OpenID-Connect.

environment.ts
export const environment = {
  production: true,
  envName: 'local',
  keycloak: {
    // Url of the Identity Provider
    issuer: 'https://auth.uat.industryapps.net/auth/realms/IndustryApps',

    // URL of the SPA to redirect the user to after login (Development Env)
    redirectUri: 'https://democustomer.uat.industryapps.net/<APPCODE>/',

    // The SPA's id. 
    // The SPA is registerd with this id at the auth-serverß
    clientId: 'APPID',

    responseType: 'code',
    // set the scope for the permissions the client should request
    // The first three are defined by OIDC.
    scope: 'openid profile email',
    // Remove the requirement of using Https to simplify the demo
    // THIS SHOULD NOT BE USED IN PRODUCTION
    // USE A CERTIFICATE FOR YOUR IDP
    // IN PRODUCTION
    requireHttps: false,
    // at_hash is not present in JWT token
    showDebugInformation: true,
    disableAtHashCheck: true
  }
};

Include an authconfig sub-directory in your project directory. Create an authconfig.module.ts file which initialises an oauth2-oidc module as illustrated below:

authconfig.module.ts
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { OAuthModule, AuthConfig } from 'angular-oauth2-oidc';

import { AuthConfigService } from './auth-config.service';
import { authConfig, OAuthModuleConfig } from './auth.config';

export function init_app(authConfigService: AuthConfigService) {
    return () => authConfigService.initAuth();
}

@NgModule({
  imports: [ HttpClientModule, OAuthModule.forRoot() ],
  providers: [
    AuthConfigService,
    { provide: AuthConfig, useValue: authConfig },
    OAuthModuleConfig,
    { 
      provide: APP_INITIALIZER, 
      useFactory: init_app, 
      deps: [ AuthConfigService ], 
      multi: true
    }
  ]
})
export class AuthConfigModule { }

Add an auth.config.ts file in the authconfig sub-directory. auth.config.ts would utilise the environment variables configured in the environment.ts file made previously.

auth.config.ts
import { AuthConfig } from 'angular-oauth2-oidc';
import { environment } from '../../environments/environment';

export const authConfig: AuthConfig = {

    // Url of the Identity Provider
    issuer: environment.keycloak.issuer,

    // URL of the SPA to redirect the user to after login
    redirectUri: environment.keycloak.redirectUri,

    // The SPA's id. 
    // The SPA is registerd with this id at the auth-serverß
    clientId: environment.keycloak.clientId,

    responseType: environment.keycloak.responseType,
    // set the scope for the permissions the client should request
    // The first three are defined by OIDC.
    scope: environment.keycloak.scope,
    // Remove the requirement of using Https to simplify the demo
    // THIS SHOULD NOT BE USED IN PRODUCTION
    // USE A CERTIFICATE FOR YOUR IDP
    // IN PRODUCTION
    requireHttps: environment.keycloak.requireHttps,
    // at_hash is not present in JWT token
    showDebugInformation: environment.keycloak.showDebugInformation,
    disableAtHashCheck: environment.keycloak.disableAtHashCheck
};


export class OAuthModuleConfig {
    resourceServer: OAuthResourceServerConfig = {sendAccessToken: false};
}

export class OAuthResourceServerConfig {
    /**
     * Urls for which calls should be intercepted.
     * If there is an ResourceServerErrorHandler registered, it is used for them.
     * If sendAccessToken is set to true, the access_token is send to them too.
     */
    allowedUrls?: Array<string>;
    sendAccessToken = true;
    customUrlValidation?: (url: string) => boolean;
}

Add an auth-config.service.ts file to the authconfig sub-directory which sets up OAuthService. auth-config.service.ts initialises a login page if authorisation is required otherwise initialises the app instance.

auth-config.service.ts
import { Injectable } from '@angular/core';
import { AuthConfig, NullValidationHandler, OAuthService } from 'angular-oauth2-oidc';
import { filter } from 'rxjs/operators';

@Injectable()
export class AuthConfigService {
    
    private _decodedAccessToken: any;
    private _decodedIDToken: any;
    get decodedAccessToken() { return this._decodedAccessToken; }
    get decodedIDToken() { return this._decodedIDToken; }

    constructor(
      private readonly oauthService: OAuthService,
      private readonly authConfig: AuthConfig,
    ) {}

    async initAuth(): Promise<any> {
      return new Promise<void>((resolveFn, rejectFn) => {
        // setup oauthService
        this.oauthService.configure(this.authConfig);
        this.oauthService.setStorage(localStorage);
        this.oauthService.tokenValidationHandler = new NullValidationHandler();
  
        // subscribe to token events
        this.oauthService.events
          .pipe(filter((e: any) => {
            return e.type === 'token_received';
          }))
          .subscribe(() => this.handleNewToken());
          
        // continue initializing app or redirect to login-page
        
        this.oauthService.loadDiscoveryDocumentAndLogin().then(isLoggedIn => {
          if (isLoggedIn) {
            this.oauthService.setupAutomaticSilentRefresh();
            resolveFn();
          } else {
            this.oauthService.initImplicitFlow();
            rejectFn();
          }
        });
        
      });
    }
  
    private handleNewToken() {
      this._decodedAccessToken = this.oauthService.getAccessToken();
      this._decodedIDToken = this.oauthService.getIdToken();
    }

}

Add auth-config.service.spec.ts to the authconfig sub-directory which is the last requirement to be added in the authconfig sub-directory.

auth-config.service.spec.ts
import { TestBed } from '@angular/core/testing';

import { AuthConfigService } from './auth-config.service';

describe('AuthConfigService', () => {
  let service: AuthConfigService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(AuthConfigService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

So far the addition of the authconfig sub-directory includes:

authconfig
.
├── auth-config.service.spec.ts
├── auth-config.service.ts
├── auth.config.ts
└── authconfig.module.ts

Be sure to import the environment into the main.ts file in your app directory. e.g.

import { environment } from './environments/environment';

A full main.ts file example is illustrated below, given you've designated both an environment.prod.ts version and environment.ts file in an environments sub-directory.

main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

A full demo sample client-side Angular app is available below provided as a reference.

Last updated