Building SPA Authentication using Angular, OpenID, Oidc-client

Use oidc-client.js to support OpenID Connect in Angular applications.

SPA authentication using OpenID Connect, Angular CLI and oidc-client

OpenID Connect is a modern authentication protocol, especially in SPA applications, or common client applications. The client I often recommend is oidc-client, which is a pure JavaScript library provided in the IdentityModel OSS project. It handles all protocol interactions with the OpenID Connect Provider, including token verification (which is strangely ignored by some libraries), and is a certified OpenID Connect Relying Party that conforms to the implicit RP and configures the RP profile.

In this article, we’ll use Angular CLI and oidc-client library to exercise basic verification scenario, we will verify a user, and then use the access token API to access using OAuth protection. Will be used implicit flow here, all tokens here are passed through the browser (be sure to remember when processing on the client side)

Angular CLI initialization

To keep the content of this article simple, we use the Angular CLI to create our Angular application, which supports basic routing. If you are not using the Angular CLI, there is no problem, the OpenID Connect implementation in this article supports all Angular 4 applications.

If you are not ready, first you need to install Angular CLI as a global package

npm install -g @ angular / cli

Then use the CLI to create an application with routing support, and skip testing support for now.

ng new angular4-oidcclientjs-example --- routing true --skip-tests

This will initialize all the support needed for our tutorial. You should already be able to run the application.

ng serve

Now, if you visit our site in a browser, the default address is http://localhost:4200 and we should already see a welcome page.

Protected components and routing guards

Protected component

Now we create a component protected that requires user authentication before we can access it. We use the Angular CLI to create the component.

ng generate component protected

After creating the component, CLI will automatically be added to the component app.module, but it needs to be added manually to the routing system in order to be able to access it. Therefore, it needs to be modified app-routing.module

import {NgModule} from  '@ angular / core' ; 
import {Routes, RouterModule} from  '@ angular / router' ;

import {ProtectedComponent} from  './protected/protected.component' ;

const routes: Routes = [ 
    { 
        path: '' , 
        children: [] 
    }, 
    { 
        path: 'protected' , 
        component: ProtectedComponent 
    } 
];

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

Here we imported the component and registered the / protected path.

Now we update app.component.html to add a navigation link to the component.

Route Guard

Now that we have the page, let’s protect it! We can use with CanActivate to achieve the guard. This means that the guard can provide processing logic before routing to decide if routing can be activated for use. Now we return first false, which will prevent access to the protected route.

Create the guard using the CLI

ng generate service services\authGuard

Then from angular/router import CanActivate, to achieve our service, and then return directly false. The minimized route guard looks like this, but you are also welcome to implement the complete logic you need.

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';

@Injectable()
export class AuthGuardService implements CanActivate {
        canActivate(): boolean {
            return false;
        }
}

Now we need app.module a NgModule registered guarding our route, which will not be done automatically.

import { AuthGuardService } from './services/auth-guard.service';

@NgModule({
    // declarations, imports, etc.
    providers: [AuthGuardService]
})

Finally, the app-routing.module use of the guard in the routing.

import { AuthGuardService } from './services/auth-guard.service';

const routes: Routes = [
  // other routes
  {
    path: 'protected',
    component: ProtectedComponent,
    canActivate: [AuthGuardService]
  }
];

Now back to the application and accessing the protected component, you should see that it is no longer accessible.

Use oidc-client for authentication

Now that we have the resources and guards, let’s create a service to handle authenticating and managing user sessions. First, create a file named AuthService services.

ng generate service services\auth

Then, in the app.module registry.

import { AuthService } from './services/auth.service';

@NgModule({
    // declarations, imports, etc.
    providers: [AuthGuardService, AuthService]
})

To handle all interactions with our OpenID Connect Provider, let’s bring in oidc-client. We can pull this in as a dependency in our package.json file with:

"oidc-client": "^1.3.0"

And we’ll also need its peer dependency of:

"babel-polyfill": "^6.23.0"

Don’t forget to make sure they install before continuing (npm update).

We now need to import UserManager, UserManagerSettings, and User into our auth service from the oidc-client library, like so:

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

UserManager

oidc-client The entrance to the library is UserManager. This is where we all interact with OpenID Connect. Another option is to use OidcClient, however, it only manages protocol support. In this article, we use UserManager to process all user management.

UserManager The constructor requires an UserManagerSettings object. We hard-code these settings here, but in production they should be initialized with your environment configuration.

export function getClientSettings(): UserManagerSettings {
    return {
        authority: 'http://localhost:5555/',
        client_id: 'angular_spa',
        redirect_uri: 'http://localhost:4200/auth-callback',
        post_logout_redirect_uri: 'http://localhost:4200/',
        response_type:"id_token token",
        scope:"openid profile api1",
        filterProtocolClaims: true,
        loadUserInfo: true
    };
}

If you are familiar with the OpenID Connect Provider, these settings should be recognizable.

  • authority is the URL of our OpenID Connect Provider
  • client_id is the client application’s identifier registered within the OpenID Connect Provider
  • redirect_uri is the client’s registered URI where all tokens will be sent to from the OpenID Connect Provider
  • response_type can be thought of as the token types requested, which in this case is an identity token that represents the authenticated user and an access token to give us access to our protected resources. The other option here is code which is unsuitable for client side/in-browser applications, as it requires client credentials to be swapped for tokens
  • scope is the scoped access which our application requires. In this case, we are asking for two identity scopes: openid and profile, which will allow us access to certain claims about the user, and one API scope: api1, which will allow us access to an API protected by this OpenID Connect Provider

These settings are necessary to create the UserManager, and we also include some optional settings:

  • post_logout_redirect_uri, Which is the URL address registered in the OpenID Connect Provider and redirected after the user logs out
  • filterProtocolClaims It protects the statement protocol level, for example, from the Identity Server as the profile data extracted from nbf, iss, at_hash and nonce. These claims are not particularly useful outside of token verification.
  • loadUserInfo allow the library to automatically access the OpenID Connect Provider using the obtained access token to obtain user information. To get additional information about authenticated users, this setting defaults to true

Currently, we use OpenID Connect metadata endpoints for automatic discovery. However, if this is not suitable for you (probably found that the endpoint does not support CORS), UserManager can also be manually configured, please see the configuration section document.

By default, the oidc-client will use the browsers session storage. This can be changed to local storage, however this can have privacy implications in some countries, as you would be storing personal information to disk. To switch to using local storage, you’ll need to import WebStorageStateStore and set the userStore property UserManagerSettings to:

userStore: new WebStorageStateStore({ store: window.localStorage })

In our AuthService using your settings to initialize UserManager.

private manager = new UserManager(getClientSettings());

Then, create an internal member to hold the current user, which will be initialized in the constructor.

private user: User = null;

constructor() {
    this.manager.getUser().then(user => {
        this.user = user;
    });
}

Here, we use the oicd-client getUser approach. This method loads the currently authenticated user by checking the store in the configuration (now the Session store). The return value of the method is one Promise, so we save the returned value to an internal member for easy access later. Here we will use the User object.

AuthService

We will create 5 methods:

  1. isLoggedIn
  2. getClaims
  3. getAuthorizationHeaderValue
  4. startAuthentication
  5. completeAuthentication

We from isLoggedIn the beginning, where we will check if we already have a user, and if not yet expired. This can be through its expired come to know the properties, it will check whether the user’s access token has expired.

isLoggedIn(): boolean {
    return this.user != null && !this.user.expired;
}

getClaims Simply return the user’s statement. It is stored in the user’s profile properties. Because we set filterProtocolClaims to true, these statements more meaningful.

getClaims(): any {
    return this.user.profile;
}

getAuthorizationHeaderValue HTTP is used to generate from the user object authorization request header. This requires using the acquired token type and the access token itself. We will see how to use it when accessing a protected API.

getAuthorizationHeaderValue(): string {
    return `${this.user.token_type} ${this.user.access_token}`;
}

In order to achieve bulky protocol interaction, we need startAuthentication() and completeAuthentication() methods.

They OpenID Connect the process of our verification request, using the oidc-client signinRedirect and signRedirectCallback methods. After the call, the settings in UserManagerSettings will be used to automatically redirect the user to the OpenID Connect Provider. You can also use use signinPopup and signinPopupCallback this will open a new window instead of redirection.

startAuthentication(): Promise<void> {
    return this.manager.signinRedirect();
}

completeAuthentication(): Promise<void> {
    return this.manager.signinRedirectCallback().then(user => {
        this.user = user;
    });
}

signInRedirect() It will generate an authorization request to our OpenID Connect Provider server, processing state and nonce, if necessary, access metadata endpoint.

After passing the verification, the callback function will be called and passed in the token, including tokenizable verification. If loadUserInfo set true, it will access the user information endpoint to obtain additional information by authorized users. This method returns the authenticated user Promise, we can save it locally.

Route Guard

Now, update our routing guard to use the newly created one AuthService, check if the user is logged in, otherwise, start the verification process.

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';

import { AuthService } from '../services/auth.service'

@Injectable()
export class AuthGuardService implements CanActivate {

    constructor(private authService: AuthService) { }

    canActivate(): boolean {
        if(this.authService.isLoggedIn()) {
            return true;
        }

        this.authService.startAuthentication();
        return false;
    }
}

Callback endpoint

We need another component to complete the verification. It is a callback component for authentication, which helps us to obtain the identity and access token returned from the OpenID Connect Provider, and uses the oidc-client library to complete the authentication process. By creating another component can be done, we call auth-callback components, use it to map redirect uri, use the CLI to create it.

ng generate component auth-callback

Then, to import our AuthService service, through constructor injection, in ngOnInit calling it’s completeAuthentication() methods.

constructor(private authService: AuthService) { }

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

Once again, we add this component to the routing system, and the mapped path is the URL we registered in the OpenID Connect Provider.

import { AuthCallbackComponent } from './auth-callback/auth-callback.component';

const routes: Routes = [
    // other routes
    {
        path: 'auth-callback',
        component: AuthCallbackComponent
    }
];

Now when we try to access a protected component, we will be automatically redirected to the OpenID Connect Provider. Once validated, we will return to our auth-callback page, our token url fragment, if you are checking session storage, should find a new name: the key, its value is JSON, which contains our identity token, access token token type and user description data. oidc.user:http://localhost:5555/:angular_spa

Redirects

Now, after user authentication, it is returned to the callback address, so the user experience is not good. Instead, we should record the protected resource address that the user is trying to access. Once returned to the application through authentication, the callback page should redirect the user back to the desired page. It depends on how you want to handle it. I’ve seen someone record addresses in session/local storage before.

Access to protected API

Currently, our protected resources are inside the application, forcing users to authorize before they can access them. But what about accessing APIs protected by the OpenID Connect Provider? As part of the verification, we have requested an access token, so we use it to authorize access to the API.

First, generate a new component where we access the API

ng generate component call-api

Then, add it to the routing system.

import { CallApiComponent } from './call-api/call-api.component';

const routes: Routes = [
    // other routes
    {
        path: 'call-api',
        component: CallApiComponent,
        canActivate: [AuthGuardService]
    }
];

We need to use HttpClientModule, in app.module import it in.

import { HttpClientModule } from '@angular/common/http';

@NgModule({
    // declarations, providers, etc.
    imports: [HttpClientModule]
})

In internal components, through the constructor to inject security services, as well as angular/common/http the HTTP service.

import { Component, OnInit } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';

import { AuthService } from '../services/auth.service'

@Component({
    selector: 'app-call-api',
    templateUrl: './call-api.component.html',
    styleUrls: ['./call-api.component.css']
})
export class CallApiComponent implements OnInit {

    constructor(private http: Http, private authService: AuthService) { }
    ngOnInit() {
    }
}

In ngOnInit the setting authorization request header, and then access the API. After we get the response, we save it to the internal members.

export class CallApiComponent implements OnInit {
    response: Object;
    constructor(private http: HttpClient, private authService: AuthService) { }

    ngOnInit() {
        let headers = new HttpHeaders({ 'Authorization': this.authService.getAuthorizationHeaderValue() });

        this.http.get("http://localhost:5555/api", { headers: headers })
          .subscribe(response => this.response = response);
    }
}

The demo API simply returns some text, which requires the use of http:5555://localhost server issued by the bearer type of api1 the token.

In the component’s html, we display these response text.

<p>
    Response: {{response}}
</p>

Finally, the main page is updated to include a link to the feature.

<h3>
    <a routerLink="/">Home</a>
    | <a routerLink="/protected">Protected</a> 
    | <a routerLink="/call-api">Call API</a>
</h3>
<h1>
  {{title}}
</h1>
<router-outlet></router-outlet>

Token Expiration

Now, if your access token expires, one of two things will happen:

  • The next time you visit a protected page, the AuthServiceservice will detect that you are logged out
  • Or get a 401 unauthorized access from the API
    The first scenario is fine, our authentication service will automatically redirect the user to the identity server for authentication, and return a new access token. However, for the second scenario, data may be lost, for example, the form data just filled out. Because when using implicit flow, we can not refresh token, we have to use another way, this is a silent refresh OIDC-client offer.

Sourcode

Github: https://github.com/scottbrady91/Angular4-OidcClientJs-Example/tree/implicit

#angular #openid

Building SPA Authentication using Angular, OpenID, Oidc-client
136.85 GEEK