Quote of the Day

more Quotes

Categories

Buy me a coffee

  • Home>
  • security>

How to authenticate user against Azure ADB2C from Angular app using oidc-client-js.

In this post, I show you how to authenticate your user against azure adb2c to obtain an id and access token. Specifically, we’ll discuss the following:

  • Create azure adb2c directory
  • Register applications in b2c tenant.
  • Define scopes and setup permissions.
  • Setup sign up and sign in user flow.
  • Authentication service.
  • Response to authentication events in component.

Please checkout the latest codes for this post here.

Also, check out the follow-up posts relating to using oidc-client-js to interact with Azure ADB2C:

Create Azure ADB2C directory

As an organization, you may store your employee accounts in the main tenant. You may not want to mix B2C accounts, which probably include users from outside of your organization and partners. Also, the authentication flow is different than that for regular azure AD because ADB2C can integrate with different social providers. For instance, as part of sign in, the user may choose to login with the user’s Facebook account.

Because ADB2C requires different authentication flow, and the B2C users are usually outside of your organization and partners, Azure ADB2C requires a separate tenant. Microsoft provides good tutorial for creating a B2C tenant. You can check it out here.

Create service principals

Essentially, a service principal is just an identity for your application in Azure. A service principal helps azure to identity your app. As part of creating the service principal, you specify the redirect urls to where azure can direct the user after authentication. Upon a success or failure authentication, azure makes sure the user only comes back to one of the redirect urls, and not just any urls to protect the user.

If you want to follow along with the implementation, you need to register two service principals, one for the angular, and one for the web API.

Register angular app for adb2c login.

Follow the similar steps to register the web API. In addition, you’ll need to give a value to identify your web api. This URI becomes the prefix for a scope that the api exposes. By defining the app uri, you’ll get the default scope – user_impersonation which you can use when requesting tokens from the angular application, or define a different scope if desires.

Register web API and define app id uri for adb2c login.

When registering the apps, make note of the client ids as you are going to need them in the codes.

For more information on app registration in ADB2C, check out the document.

Caution: At the time of writing, the new experience – App registrations blade for registering applications in azure AD is available. However, this feature is still in preview and is buggy in my experience using it. I tried registering the applications via App registrations and could not get login to work. The error I got was:

The provided application with ID ‘6923da1f-e3e5-4d72-be4d-b6733b58ee79’ is not valid against this service. Please use an application created via the B2C portal and try again.

Microsoft may have already fixed this error by the time you read it. If you get the error above, try to register using Applications blade as shown in the above animations.

Setup permissions

You need to let the framework know that the angular app needs access to the web API by adding the permission, which refers to the scopes you have created for the web api or the default scope you get when setting the app id uri for the web api . To add the permission, follow the steps below:

  1. In the app registration for angular, click Api Access.
  2. Click the Add button to search for available permissions.
  3. Under Select API, select the web api you have registered.
  4. Select the scopes.
  5. Click OK to add the permission.
Add permission for angular app to access web API

Create B2C User flow for sign in

  1. Go to your B2C tenant, under Policies on the left side menu, select User flows.
  2. Select New user flow to add a new flow
  3. Select sign up and sign in to create a template with links to sign in and register a new account.
  4. Fill out the form to create the flow. Azure ADB2C allows you to customize the sign in process up to a point. For example, you can select which claims you want to include in the access token, whether to use Multi Factor authentication etc. As part of the flow creation, you can select one or more identity providers. These are the providers you configure separately in the Provider section of azure ADB2C. You must select at least one provider. If you haven’t set up any thing, you can use Email signup as that is the default provider.
Create ADB2C sign in flow

Install oidc-client-js

oidc-client-js is a Javascript based library that implements OpenID Connect. The library is pretty solid. I have used it to successfully integrate my angular applications to both Azure AD and Azure ADB2C without major hurdles. The library provides great abstractions to interact with Azure ADB2C, exchange token and manage the user’s session.

From the project’s github page, you need Node version 4.4 and above. To install, simply run the command below:

npm install oidc-client --save

Authentication service

Oidc-client-js exposes high level interfaces to manage the user’s session including login, logout, token renewal and provides hooks for various events such as user loaded, user unloaded, token expired, and session changed. It is a good idea to have your own service class in which you use the UserManager class and implement business logic to act on the different authentication events as necessary. This is the pattern I follow from the sample project on the the github page of the library, which you can checkout here. Having your own class to abstract away authentication also reduces the coupling of your project and makes it easier to switch to another library if necessary.

Below shows my auth.service.ts class

import { Injectable } from '@angular/core';
import { UserManager, User } from 'oidc-client';
import { environment } from '../../environments/environment'; 
@Injectable({
  providedIn: 'root'
})
export class AuthService {

  _userManager: UserManager; 

  constructor() { 
      this.instantiate(); 
  }

  public async loginRedirect(): Promise<any> {
      return await this._userManager.signinRedirect(); 
  }

  public async loginSilent(): Promise<User> {
    var user = await this._userManager.signinSilent(); 
    return user; 
  }


  public async logoutRedirect(): Promise<any> {
      await this._userManager.signoutRedirect();
      await this._userManager.clearStaleState(); 
  }

  public addUserUnloadedCallback(callback): void {
      this._userManager.events.addUserUnloaded(callback);
  }

  public removeUserUnloadedCallback(callback): void {
      this._userManager.events.removeUserLoaded(callback);
  }

  public addUserLoadedCallback(callback): void {
      this._userManager.events.addUserLoaded(callback);
  }

  public removeUserLoadedCallback(callback): void {
      this._userManager.events.removeUserLoaded(callback);
  }

  public async accessToken(): Promise<string> {
      var user = await this._userManager.getUser();
      if (user == null) {
          throw new Error("User is not logged in");
      }
      return user.access_token;
  }


  public async getUser(): Promise<User> {
      return this._userManager.getUser();
  }

  public async handleCallBack() {
      var user = await this._userManager.signinRedirectCallback(); 
      console.log("Callback after sigin handled.", user);
  }

  public instantiate() {
      var settings = environment.oidcSettings;
      this._userManager = new UserManager(settings);
  }
}

For the most part, most of the methods are basically wrappers which call the corresponding methods in the UserManager class. In the instantiate() method, I instantiate the UserManager class with the settings I load from the environment.

Instantiate UserManager

If you look at the source codes of oidc-client-js, the settings has type UserManagerSettings which extends from OidcClientSettings. The class captures the metadata about the different components of an OIDC flow. For instance, for an implicit flow, such data can include the client id, the authorization/token endpoint, the scopes, whether to load the user info, etc …

export const environment = {
  production: false,

  oidcSettings: { 
    client_id : "{client id from the app registration for angular}", 
    authority: "https://{your-tenant-name}.b2clogin.com/tfp/{your-tenant-ID}/{policyname}", 
    response_type: "id_token token", 
    post_logout_redirect_uri: "https://localhost:4200/", 
    loadUserInfo: false,
    redirect_uri: "http://localhost:4200/", 
    silent_redirect_uri: "http://localhost:4200/", 
    response_mode: "fragment", 
    scope: "https://{your-tenant}.onmicrosoft.com/{your-web-api-app-id}/{scope-you-defined-in-app-registration-for-your-web-api} openid profile" }
};

client_id: This is the client id of the service principal you registered in your ADB2C directory to represent your angular application.

authority: This is the URL to where your app sends the user for sign in. From the document,

The sign-in URL, called an Authority, is a combination of the tenant name and policies defined in the Constants.cs file

Authenticate Users with Azure Active Directory B2C

I have found the authority url to be confusing as I have seen different patterns of the URL from different documents. However, the one that works has this format:
$"https://{your-tenant-name}.b2clogin.com/tfp/{your-tenant-ID}/{policyname}"
.

You can checkout this document to learn more.

response_type: Indicates what type of token you want to request. You can combine multiple types by space. For the implicit flow, you want to request both an access token and an id token. The id token is for identifying the user, and the access token represents authorization to access the specific API as reflected in the scopes.

If you want to learn more about the different response types, I have found this post to be clear and concise.

post_logout_redirect_uri: This is the URL to which Azure redirects the user after the user has signed out from their account.

redirect_uri: This is the URL to where Azure redirects the user after authentication. On successful authentication, the URL contains the id and access token. If an error occurs, the URL contains information about the error, also in the URL fragment. This URL needs to match the redirect_uri you set in the service principal you registered on Azure for your application.

silent_redirect_uri: This is the URL to where azure ADB2C sends a new token upon request. A token normally expires after a short period of time. On token expiration, you can send a hidden, sign in request which does not require the user’s interaction to renew the token.

response_mode: How you want Azure ADB2C to deliver the tokens. Valid values for SPA are: query or fragment.

  • Query: The tokens are in the url query parameter, after the question mark. This mode is arguably less secure because the browser’s history may record the full url which includes the tokens.
  • Fragment: The tokens come after the hash (#) sign. This is the default and probably the most appropriate for single page application. Fragment mode is more secure than query mode because it does not appear in the browser’s history or HTTP requests.
  • Form_post: This mode is only valid on the server. Because a Javascript based SPA cannot process a POST request.

scope: The type of resource your app wants to access from the server. You separate multiple scopes by space. In the above json snippets, one of the scopes is the resource URI which contains the client id of the web API that hosts the resource. The URI comes from the “Expose an API” section when registering the service principal for the web API. The value of audience in the resulting token also includes the client id of the web api.

Response to authentication events in component

Oidc-client-js provides several hooks you can use to response to authentication events such as on login, logout, token renewal etc … For the list of the available events, checkout the UserManagerEvents class of the library.

The code snippets below show how I register the callbacks so I can react when the user login and when the user logout. When the user logins, I display the name of the user, and when the user logs out, I show the Login button.

import { Component, OnInit } from '@angular/core';
import { User } from 'oidc-client';
import { Router, ActivatedRoute } from '@angular/router';
import { AuthService } from './services/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  
  user: User; 

  // callback method to execute when the user logs out. 
  private userUnloadedCallback: () => void; 
  // callback method to execute when the user logs in. 
  private userLoadedCallback: (user: User, router: Router, route: ActivatedRoute) => void; 
  
  constructor (private router: Router, private route: ActivatedRoute, 
    public authService: AuthService) {

    }

    private isTokenInURL(url: string) {
      return url.includes("id_token") || url.includes("access_token");
    }


     async ngOnInit(): Promise<void> {
      this.userUnloadedCallback = this.onUserUnLoadedCallback(this);
      this.userLoadedCallback = this.onUserLoadedCallback(this);
      this.authService.addUserUnloadedCallback(this.userUnloadedCallback);
      this.authService.addUserLoadedCallback(this.userLoadedCallback);
      this.user = await this.authService.getUser(); 

      if (this.isTokenInURL(this.router.url)) {
          this.authService.handleCallBack(); 
      }
    }


    private onUserLoadedCallback(instance: AppComponent) {
      return async function (user: User, router, route) {
        console.log("OnUserLoadedCallback(). Got user: ");
        console.log(user);
        instance.user = user; 
      }
    }

    private onUserUnLoadedCallback(instance: AppComponent) {
      return async function() {
        console.log("OnUserUnloadedCallback().");
        instance.user = null;
      }
    }

    private async getUserJson() {
      return JSON.stringify(await this.authService.getUser()); 
    }

    login() {
      this.authService.loginRedirect().then(user => {
        console.log("User logged in. Name: " + user.profile.name);
      }); 
    }

    logout() {
      this.authService.logoutRedirect().then(); 
    }
}

In ngOnInit method, notice that I register the callback functions which oidc-client-js library calls on user login and logout. Another thing i do is checking to see if the url in the address bar contains and id or access token. If the url contains the tokens, that means the user has successfully authenticated, and azure b2c has sent the user back to the application along with the tokens. I also have to let oidc-client-js library knows to handle the tokens by calling the handleMethod in the authentication service. Otherwise, the tokens are there but nothing happens.

Result

If everything is setup correctly, you’ll be redirect to the default b2c login page for authentication when clicking on login and getting back the tokens in the app.

Successful authentication against ADB2C.

If you experience errors, don’t stress too much as few things rarely work the first time. You may find the next section helpful as it lists some of the errors I have encountered when integrating with b2c to prepare the sample project for this post.

Errors you may get

Below I list some of the errors I have encountered while integrating azure adb2c to my application.

Access to XMLHttpRequest at ‘https://{yourtenant}.b2clogin.com/{yourtenant}.onmicrosoft.com/b2c_1_asignup_signin/v2.0/well-known/openid-configuration’ from origin ‘http://localhost:4200’ has been blocked by CORS policy. No ‘Access-Control-Allow-Origin’ header is present on the requested resource’.

The error above was because of the typo I had in the policy portion part of the authority URL. Once I realized and fixed the typo, the error went away. So pay attention to authority URL, which should have this pattern: $"https://{your-tenant-name}.b2clogin.com/tfp/{your-tenant-ID}/{policyname}", as per this document.

AADB2C90068: The provided application with ID ‘6923da1f-e3e5-4d72-be4d-b6733b58ee79’ is not valid against this service. Please use an application created via the B2C portal and try again.

I got this error because I registered the angular application using App Registrations blade which is still in preview as of this writing and could have bugs. I had to register the application again using the regular Applications under Manage to be able to hit the b2c login url.

AADB2C90205: This application does not have sufficient permissions against this web resource to perform the operation.

You may get this error if you haven’t defined the scope for the web api and/or add the scope to the list of the permissions the angular app needs. See the section Setup Permissions above.

Hopefully, you have found this post useful. Feel free to let me know in the comments if you have feedbacks or questions.

References

Tutorial: Register an application in Azure Active Directory B2C

oidc-client-js npm package

oidc-client-js github page

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

Configure OpenID Connect provider settings for portals

OpenID Connect Response Types

Why can’t I use query Response Mode with id_token Response Type (“implicit” flow)?

Permissions and consent in the Microsoft identity platform endpoint

Authority for a B2C tenant and policy

Possible to route received POST requests.

3 comments