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:
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:
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.
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.
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.
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.
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:
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
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.
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.
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.
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.
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.
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.
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.
Tutorial: Register an application in Azure Active Directory B2C
SPA Authentication using OpenID Connect, Angular CLI and oidc-client
Configure OpenID Connect provider settings for portals
Why can’t I use query Response Mode with id_token Response Type (“implicit” flow)?
Permissions and consent in the Microsoft identity platform endpoint
Enhancing ASP.NET Core/Blazor App Security and Reusability with HttpMessageHandler and Named HttpClient
Cache angular components using RouteReuseStrategy
Using MSAL angular to authenticate a user against azure ADB2C via authorization code flow with Proof Key for Code Exchange.
Using Azure Application Insights for centralized logging
Building multitenant application – Part 3: Authentication
Building multitenant application – Part 1: Multitenant database using Row Level Security
Migration from Oracle to azure SQL caveat – Azure SQL does not support time zone settings
Migrating from Oracle to Azure SQL caveat – prepared statement set string causes implicit conversion