How to implement OIDC authentication with Angular and ngRX
Using angular-oauth2-oidc library to automate the JWT token handling
We all keep hearing OIDC here, OIDC there, but what does exactly mean?
Following Microsoft’s definition it stands for OpenId Connect (OIDC) happens to be an authentication protocol, which is and extension of open authorisation (OAuth 2.0), standardising sign in process for accessing digital services.
Another words, a mechanism to help verifying users within unrelated applications without sharing user data. This way user has to sign in once and is able to access multiple applications.
Such mechanism has been already popular for quite a while and organisations already started to incorporate it into their ecosystems.
Ok, no we know the basic concepts and we got the grasp of what type of problem OIDC is aiming to solve, and its goal is being achieved in following steps:
- user enters specific application and is being redirected to OpenID provider
- user provides username and password
- user credentials are being passed to OpenID provider
- provider verifies credentials and obtains authorisation
- user is being redirected to the original application with identity token
Sounds complex right?
But it is not as difficult to implement as it seems. Off course if you try to build your own solution without any external libraries involved, it would take a little bit more time to achieve it. Although it isn’t a rocket science, you don’t have to do it entirely on your own. There’s a angular-oauth2-oidc library provided by Manfred Steyer (who doesn’t need any introduction in Angular world :-)).
The library supports wide range of Angular versions including both module based and standalone approach. All you need to do is to set it up properly and you do not have to worry about redirections, adding token to requests headers and even token renewal!
Let’s start with module based approach first, but before then we need to add library to our project dependencies:
npm i angular-oauth2-oidc --save
Once the library is successfully installed, we need to make some preparations in order to set up the configuration and handling.
I will use already existing implementation of user feature store to handle authorization. If you’re not familiar with that topic you can refer to my other article:
Now, when you’re on the same page, let’s extend our user feature store to handle OIDC.
First let us extend our UserState
with some additional attributes within user.state.ts
file like below:
export interface UserState {
// ... other attributes
loggedIn: boolean;
logInRequestHandled: boolean;
}
export const initialState: UserState = {
// ... other attributes
loggedIn: false,
logInRequestHandled: false,
};
Let us also create a selector to get our information back from the store within user.selectors.ts
file:
export const selectIsLogInRequestHandled = createSelector(
selectUserState,
({ logInRequestHandled }: UserState) => logInRequestHandled
);
We need to define some actions, which will help us navigating the entire process within user.actions.ts
file like so:
const user = '[User]';
// ... other actions
export const logIn = createAction(`${user} LogIn`);
export const logInSuccess = createAction(`${user} LogIn Success`);
export const logInError = createAction(`${user} LogIn Error`);
Our actions have to impact our state, so have to define how these will impact the state changes within user.reducer.ts
file:
import { createReducer, on } from '@ngrx/store';
import {
initialState,
logInSuccess,
UserState,
logInError,
} from './index';
export const userReducer = createReducer(
initialState,
// ...some other actions
on(logInSuccess, (state: UserState) => ({
...state,
loggedIn: true,
logInRequestHandled: true,
})),
on(logInError, (state: UserState) => ({
...state,
loggedIn: false,
logInRequestHandled: true,
}))
);
We are almost there with the set up, we need to update our UsersFacade
within user.facade.ts
file and we can move to put it altogether.
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { UserState } from './user.state';
import { logIn } from './user.actions';
import { selectIsLogInRequestHandled } from './user.selectors';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class UserFacadeService {
readonly logInRequestHandled$: Observable<boolean> = this.store.select(selectIsLogInRequestHandled);
constructor(private readonly store: Store<UserState>) {} // you can use injection token instead of constructor
logIn(): void {
this.store.dispatch(logIn());
}
}
Now let us set the stage up, we are one step away from putting it altogether, we need to define the asynchronous operations within our user.effects.ts
file like so:
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { EMPTY, from, iif } from 'rxjs';
import {
getUserSettings,
logIn,
logInError,
logInSuccess,
} from './index';
import { OAuthErrorEvent, OAuthEvent, OAuthService, OAuthSuccessEvent } from 'angular-oauth2-oidc';
import { environment } from '../../../environments/environment';
@Injectable({ providedIn: 'root' })
export class UserEffects {
constructor(
private readonly actions$: Actions,
private readonly router: Router,
private readonly oAuthService: OAuthService
) {
this.oAuthService.configure(environment); // provider configuration
this.oAuthService.setupAutomaticSilentRefresh(); // silent automated token refresh, otherwise your token gets outdated and will not be refreshed
}
listenOAuth$ = createEffect(() =>
this.oAuthService.events.pipe(
mergeMap((event: OAuthEvent) => {
if (event instanceof OAuthErrorEvent) {
return [logInError()];
}
if (event instanceof OAuthSuccessEvent && event.type === 'token_received') {
return [logInSuccess()];
}
return EMPTY;
})
)
);
logIn$ = createEffect(() =>
this.actions$.pipe(
ofType(logIn),
mergeMap(() =>
iif(
() => this.oAuthService.hasValidIdToken() && this.oAuthService.hasValidAccessToken(),
[logInSuccess()],
from(this.oAuthService.loadDiscoveryDocumentAndLogin()).pipe(
tap((result: boolean): void => {
if (!result) {
this.oAuthService.initCodeFlow();
}
}),
mergeMap(() => EMPTY),
catchError(() => [logInError()])
)
)
)
)
);
logInSuccess$ = createEffect(() => this.actions$.pipe(ofType(logInSuccess), map(getUserSettings)));
logInError$ = createEffect(
() =>
this.actions$.pipe(
ofType(logInError),
tap(() => this.router.navigate(['no-access']))
),
{ dispatch: false }
);
}
So, now onto the angular-oauth2-oidc
library implementation. We need a config file with the initial information we pass in order to set up our provider information to the library itself:
import { AuthConfig } from 'angular-oauth2-oidc';
export const authCodeFlowConfig: AuthConfig = {
// Url of the Identity Provider
issuer: 'https://idsvr4.azurewebsites.net',
// URL of the SPA to redirect the user to after login
redirectUri: window.location.origin + '/index.html',
// The SPA's id. The SPA is registerd with this id at the auth-server
// clientId: 'server.code',
clientId: 'spa',
// Just needed if your auth server demands a secret. In general, this
// is a sign that the auth server is not configured with SPAs in mind
// and it might not enforce further best practices vital for security
// such applications.
// dummyClientSecret: 'secret',
responseType: 'code',
// set the scope for the permissions the client should request
// The first four are defined by OIDC.
// Important: Request offline_access to get a refresh token
// The api scope is a usecase specific one
scope: 'openid profile email offline_access api',
showDebugInformation: true,
};
This is a basic implementation from the library docs, however you’re configuration might differ based on different environments your application will be deployed into, so I propose to keep it within the environment.${specific}.ts
file.
Module based approach:
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { UserFacadeService } from './store';
import { OAuthModule } from 'angular-oauth2-oidc';
import { filter } from 'rxjs/operators';
import { HttpClientModule } from '@angular/common/http';
function initializeLogIn(userFacade: UserFacadeService): () => void {
return (): Observable<boolean> => {
userFacade.logIn();
return userFacade.logInRequestHandled$.pipe(filter(Boolean)));
};
}
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
OAuthModule.forRoot({
resourceServer: { // You can pass the array of URIs entitled to include the authorisation token or allow all requests by not passign itat all.
sendAccessToken: true,
},
}),
],
providers: [
{
provide: APP_INITIALIZER,
useFactory: initializeLogIn,
deps: [UserFacadeService],
multi: true
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
Let’s analyze the code above. We’ve declared initializeLogIn
APP_INITIALIZER and passed it along within providers array within the main application module. This is where the logIn
attempt is being called and we wait until logInRequestHandled$
Observable from our UserFacade
source emits true
.
Not sure what’s APP_INITIALIZER? Check out my other article related to that very topic:
Also within main application module we import HttpClientModule
from Angular common library along with the OAuthModule
from the library we’ve just installed.
Important: Make sure your other modules do not import
HttpClientModule
on their own, otherwise you’ll end up with multiple instances and not every API request will be equipped with theAuthorization
header withBearer ${token}
as value.
Standalone approach Angular v15:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { provideOAuthClient } from 'angular-oauth2-oidc';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(),
provideOAuthClient(),
{
provide: APP_INITIALIZER,
useFactory: initializeLogIn,
deps: [UserFacadeService],
multi: true,
},
]
});
Standalone approach Angular v14:
import { bootstrapApplication } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { provideOAuthClient } from 'angular-oauth2-oidc';
import { importProvidersFrom } from '@angular/core';
bootstrapApplication(AppComponent, {
providers: [
importProvidersFrom(HttpClientModule),
provideOAuthClient(),
{
provide: APP_INITIALIZER,
useFactory: initializeLogIn,
deps: [UserFacadeService],
multi: true,
},
]
});
The difference is that standalone components in Angular v14 were still experimental phase and not every functionality had its own provider back then.
And that’s it, your ngRX based OIDC implementation is up and running!
That was easy wasn’t it?
Need consultancy on your project? Do not screw up your app and get in touch :-)