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: