Quote of the Day

more Quotes

Categories

Get notified of new posts

Buy me coffee

  • Home>
  • security>

Using oidc-client-js to obtain tokens from Azure AD (v1.0) or Microsoft identity platform (v2.0) .

Published August 14, 2019 in Angular , OAuth2 , OpenID Connect , security - 1 Comment

In my previous post, I mention using MSAL for angular to implement implicit flow in angular application. However, MSAL is still in preview and I could not get it to work in IE 11. In addition, I could not find a way to obtain both access and id tokens in a single call. I have switched to oidc-client-js. Besides adding the polyfills for IE, I did not have to do much for oidc-client-js to run in IE11. The library also allows me to configure response_type parameter of a request to the authorization endpoint to obtain both id and access tokens in one call. Overall, I have found the library to be more stable than MSAL for angular. In this post, I share how I configure oidc-client-js in an angular application to obtain tokens from Azure Active Directory (v1.0 endpoint) as well as some of the lessons I have learned.

What is oidc-client-js?

Oidc-client-js is a javascript library developed mainly by Brock Allen and Dominick Baier. The library implements OpenID Connect (oidc) implicit flow Here is the description from the github :

Library to provide OpenID Connect (OIDC) and OAuth2 protocol support for client-side, browser-based JavaScript client applications. Also included is support for user session and access token management.

oidc-client-js

Installing oidc-client-js

Here is the command to install the library using node package manager. If you need more info, checkout the github page.

npm install oidc-client --save

In case you are new to angular, you run the command at the root of your angular directory where package.json file resides.

Configuring oidc-client-js

The library contains a number of classes. However, the main one you probably use is the UserManager class which abstracts away much of the complexity and provides a high level interface for sign in, sign out, obtain tokens and manages session. You need to provide a number of settings such as client id, authorization endpoint, response type, redirect url etc … In addition, I have found it is necessary to implement a few callbacks when using popup for sign in and sign out . As such, it is a good idea to create a dedicated service class in which you can configure oidc-client-js.

Below codes show how I configure oidc-client to obtain tokens from Azure Active Directory (v1) endpoint.

public async instantiate(): Promise<AuthService> {

    var redirectUrl = `https://${window.location.hostname}` + (window.location.port ? `:${window.location.port}` : "") + "/home";
    var postLogoutUrl = `https://${window.location.hostname}` + (window.location.port ? `:${window.location.port}` : "");
    var userStore = new WebStorageStateStore({ store: localStorage });

    var settings = {
      authority: environment.AAD.Authority,
      client_id: environment.AAD.ClientID,
      redirect_uri: redirectUrl,
      silent_redirect_uri: redirectUrl,
      post_logout_redirect_uri: postLogoutUrl,
      response_type: "id_token token",
      scope: `${environment.AAD.API.Scope} openid profile`,
      extraQueryParams: {
        resource: environment.AAD.API.Identifier
      },
      // setting loadUserInfo to true causes error because CORS 
// blocks the call to the user info endpoint. loadUserInfo: false, userStore: userStore }; this._userManager = new UserManager(settings); await this.registerCallbacks(); return this; }

Below shows sample format of the values in environment.ts:

AAD: {
    ClientID: "The client id of the angular app registered in azure ad",
TenantID: "Guid of the azure ad tenant"
// v1.0 endpoint Authority: "https://login.microsoftonline.com/{TenantId}/", API: { ClientID: "The client id of the API registered in azure ad", Identifier: "api://{AAD.API.ClientID}", Scope: "AccessAsUser" } }

A few things worth mentioning:

1.The “resource” parameter is required when requesting an access token from Azure Active Directory (v1.0) endpoint.

The extraQueryParams key contains the “resource” parameter of which the value is the app identifier of the API I registered in azure active directory. Basically, you need to provide “resource” parameter when calling the v1 authorization endpoint to obtain an access token.

From oidc-client-js documentation,

extraQueryParams: (object): An object containing additional query string parameters to be including in the authorization request. E.g, when using Azure AD to obtain an access token an additional resource parameter is required. extraQueryParams: {resource:"some_identifier"}

oidc-client-js documentation

If you don’t include the “resource” parameter when requesting an access token from v1 endpoint, you get the following error:

ErrorResponse: AADSTS500013:+Resource+identifier+is+not+provided

To learn more about the differences between v1 and v2 endpoints, checkout the documentation.

The ClientID values refer to the ids of the applications you register in azure active directory, one for the angular application and one for the API which powers the angular app. The API.Identifier is the base URI of the API. You get to set this value or use the one azure assigned under “Expose an API” in azure portal. The value azure assigns has the pattern “api://{clientId}” where clientId is the client id of the api registered in azure.

Defining scopes for an API application in Azure Active Directory via "Expose an API".
Defining scopes for an API application in Azure Active Directory via “Expose an API”.

The request oidc-client-js generates look like the following:{AAD.Authority}/oauth2/authorize?client_id={AAD.ClientID}&redirect_uri=https%3A%2F%2Flocalhost%3A44375%2Fhome&response_type=id_token%20token&scope=AccessAsUser%20openid%20profile&state=81a972a2601746cdbc5b740cc763a380&nonce=9b6f3b83918f41ad8d2e8ab3c62b70d8&display=popup&resource={AAD.API.Identifier}

which matches with the sample request in the documentation

https://login.microsoftonline.com/{tenant}/oauth2/authorize?
client_id=6731de76-14a6-49ae-97bc-6eba6914391e
&response_type=code
&redirect_uri=http%3A%2F%2Flocalhost%3A12345
&response_mode=query
&resource=https%3A%2F%2Fservice.contoso.com%2F
&state=12345

2. The callbacks are necessary

When using popup for login and logout, I have found it is necessary to register the callbacks. Otherwise, the popups do not close automatically after Microsoft redirects the user back to the app.

You can register the callbacks in two ways, either in a TypeScript service class, or via a Javascript/HTML file. Initially, I registered the callbacks in my angular service, along with the other codes that configure the library. However, I have learned that using Javascript/HTML is the better way, as the browser does not have to load your angular app when it redirects the user back to the app.

2.1 Registering the callbacks (The not so good way)

Below show the registerCallback() method in my angular service:

private async registerCallbacks() {
var url = null;
var keepOpen = false; this._userManager.signoutPopupCallback(url, keepOpen).then(_ => { }); this._userManager.signinPopupCallback().then(result => { }); this._userManager.signinSilentCallback().then(result => { }).catch(err => { console.log("signinSilentCallback() failed. " + err); }); }

Registering the callbacks in a manner similar to the above codes is not a good idea. I ran into several issues because of calling the callback methods in angular code.

  1. Because of the callbacks, when the browser redirects the user back to the app, it unnecessarily loads the angular codes again and degrades performance.
  2. The registerCallbacks()method returns a Promise and I call it from the constructor, which is a bad idea. I realized that it is not possible to wait for Promises to resolve in a constructor. Depending on the timings, I run into intermittent issues because the async methods have not finished.

Constructor must return instance of the class it ‘constructs’ therefore its not possible to return Promise<…> and await for it

Async constructor functions in TypeScript.

Before I realized I should call the callback methods outside of angular, as a workaround, I used APP_INITIALIZER provider to wait for the async codes to finish before letting other classes use the AuthService via dependency injection.

 providers: [
   /// ....
    {
      provide: APP_INITIALIZER,
      useFactory: appInitializer,
      deps: [AuthService],
      multi: true
    },
   // ...
]

export function appInitializer(authService: AuthService) {
return () => authService.instantiate();
}

APP_INITIALIZER is a special provider which waits for Promises to resolve. As part of initializing the provider, I call the instantiate() method of the AuthService class. This way, I can be sure that all the async calls in AuthService are finished before the service is available via dependency injection. You can learn more about using APP_INITILIZER to wait for promises here.

2.2 Registering the callbacks (The better way)

The better way of registering the callbacks is via HTML files. Indeed, the github repository of the library contains the sample angular project which demonstrates using this approach. You can find the sample here.

Following the sample, I added two files under the assets folder: signin-callback.html and signout-callback.html. I literally copied the codes from the sample project without modifications, and they work with no issue.

signin-callback.html

<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.5.2/oidc-client.js"></script>
<script>
  var mgr = new Oidc.UserManager();
  mgr.signinPopupCallback().then(() => {
        window.history.replaceState({},
            window.document.title,
            window.location.origin);
        window.location = "/";
    }, error => {
        console.error(error);
    });
  mgr.signinSilentCallback().then(() => {
    }, error => {
        console.error(error);
    });
</script>

signout-callback.html

<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.5.2/oidc-client.js"></script>
<script>
  var mgr = new Oidc.UserManager();
  mgr.signoutPopupCallback(null, false).then(() => {
    }, error => {
        console.error(error);
    });
</script>

In azure active directory, I just had to update the redirect urls to point to the html files.

3. Obtain tokens from Microsoft identity platform (v2.0)

Oidc-client-js also works fine with the v2.0 endpoint. The v2.0 endpoint uses scope, not resources. Therefore, you just need to update the settings:

  1. Remove the extraQueryParams key in the settings for UserManager.
  2. For authority, use the endpoint for v2.0. You can find this url under Endpoints in your Azure AD tenant. Such endpoint has the following format:

https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize

3. For scope, use the full URI which consists of the app/client id of the target api and the scope name. Such value by default has the following format:

api://{appId}/{scopeName}

For example, api://1234abcd-12df-abcd-fghi-jklmnopqrstu/AccessAsUser

References

oidc-client-js github repository

Why update to Microsoft identity platform (v2.0)

Silent Refresh – Refreshing Access Tokens when using the Implicit Flow

How to call an asynchronous service before bootstrap

1 comment