DEV Community

Cover image for Azure Active Directory B2C With PKCE for Your Angular App
Yuri Burger
Yuri Burger

Posted on

Azure Active Directory B2C With PKCE for Your Angular App

Let's create and integrate an Angular app with Azure Active Directory Business to Consumers using the Authorization Code with Proof Key of Code Exchange flow.

Although this post works with an Angular App, the concepts (including the twists and tweaks) needed to make it work for Azure AD B2C are universal for Single Page Applications. So even if you have Vue or React, this post could be useful.

Why this post (and why not just use the well known Implicit flow)? The browser based Implicit flow is old and The OAuth Working Group has published a document recommending replacing the Implicit flow with the newer Authorization Code flow with Proof Key for Code Exchange (we like to simply refer to it as the PKCE flow).

Azure AD B2C still supports the Implicit flow (as it has for a long time) but it recently began to recommend the PKCE based flow when creating new apps. So now seems like the perfect time to go along with this and start using it too. This blogpost should get you up and running for new apps, but refactoring apps that worked with the Implicit flow shouldn't be too hard. If you happen to use one of the OpenID Connect certified libraries, the changes to your apps codebase are minimal!

But what is this new PKCE flow? It is basically an enhanced version of the Authorization Code flow. To illustrate, follow me through steps in the diagram. This flow is not complex, but understanding this will benefit you if you ever need to troubleshoot login issues.

Alt Text

  1. The user clicks a login link or button. The app generates a random code_verifier and derives a code_challenge from that verifier.
    The app then redirects the user to the Azure AD B2C Authorize endpoint with the code_challenge.

  2. The user is redirected to the login page. After supplying the correct credentials the user is redirected back to the app with a authorization code.

  3. The app receives the code and posts this code along with the code_verifier (from step 1) to the Azure AD B2C Token endpoint to request an access and id token. After validation Azure AD B2C sends both these tokens back to the app.

  4. The user can now request data from the API and the app will send the access token with the request.

Setting up the stage (on Azure AD B2C)

This is a complete walkthrough, so contains a lot of steps. If you already have a working Azure AD B2C setup, skip to the next part.

First we register a new application. Two things are important, the rest can be left with the defaults:

  • Supported account types must be the option that enables the user flows
  • The Redirect URI must be of type Single-page-application (SPA) otherwise we would not have PKCE enabled and instead need to fallback on the Implicit flow.

Alt Text

After we create the application, we need to enter any additional Redirect URIs we require. In case of this demo, we add http://localhost:4200/index.html as this matches our Angular development setup.

Alt Text

To be able to request access tokens, we need to setup and expose an API using a scope. Start by "Exposing an API" and setting a App ID URI. This needs only to be done once and the URI must be unique within your Azure AD B2C tenant.

Alt Text

After the URI we can continue adding API scope(s).

Alt Text

Before we can actually request an API scope, the permissions must be added. API Permissions, Add a permission, My APIs
And, because we want to skip the consent forms, we grant admin consent for this permission.

Alt Text

Alt Text

And finally we take note of the Application (client) ID from the overview page. We need this value later configuring our Angular app.

Alt Text

Setting up the User Flows (on Azure AD B2C)

User Flows are configurable login/logout/reset experiences. They are (somewhat) customizable and provide us with ready to go multi-language templates for our users. So we set up two of them:

First a flow for signing up (registration) and signing in (login). This flow enables both in one universal form.

Alt Text

Alt Text

In my case I enable the Local Accounts, so the user objects will be stored in my Azure AD B2C tenant.

The second flow enables self-service password reset. This flow requires some tweaking in our app, but that is covered in the last part.

Alt Text

Since we have Local Accounts, we enable that option.

Setting up your app (with Angular)

There are a few OAuth/OpenID Connect Angular libraries out there, but for my projects (including this demo) I have picked the excellent library from Manfred Steyer. Just follow the "Getting Started" documentation or take a look at the demo app.

More info: https://manfredsteyer.github.io/angular-oauth2-oidc/docs/index.html

Couple of things are important:

  • You need the clientid from the new Azure AD B2C app that was created earlier;
  • You also need the custom scope from that was created together with the app;
  • We need a additional steps to be able to succesfully login with PKCE. See the next section for this.

The twist and tweaks with Azure AD B2C

Up until this point, things are pretty straightforwared. And if you were to run this example on any of the other well known Identity Service Providers, you would be finished after completing the previous part. For Azure AD B2C we need to do some additional configuration and coding to make things work well.

Issue 1: disable strict document validation

The mentioned library uses a feature called strictDiscoveryDocumentValidation by default. This ensures that all of the endpoints provided via the Identity Provider discovery document share the same base URL as the issuer parameter. Azure AD B2C provides different domains or paths for various endpoints and this makes the library fail validation. To use this library with Azure AD B2C we need to disable this document validation.

There is a property for this in the AuthConfig, just set the "strictDiscoveryDocumentValidation: to "false"

Issue 2: support the password reset flow

This one ended up being pretty ugly, especially for the PKCE flow. So what's the deal?

Microsoft uses a feature called Linking User Flows. What happens is, that if you click the "Forgot password" option in the login form, Microsoft will redirect the user back to your app with a special error code.

A sign-up or sign-in user flow with local accounts includes a Forgot password? link on the first page of the experience.
Clicking this link doesn't automatically trigger a password reset user flow. Instead, the error code AADB2C90118 is returned to your application. Your application needs to handle this error code by running a specific user flow that resets the password.

Read more about this here: https://docs.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview#linking-user-flows

So we need to ensure that if a user has clicked the Forgot Password link, we send them on the right path back to Azure AD B2C. Ok, that is where the second flow we created comes into play. This flow has exactly the same base URL but uses a different profile. In our case "b2c_1_passwordreset" instead of "b2c_1_signupandsignin". We do this by noticing the error code and overriding the authorize endpoint:

if (this.userHasRequestedPasswordReset(err)) {
    // In this case we need to enter a different flow on the Azure AD B2C side.
    // This is still a valid Code + PKCE flow, but uses a different form to support self service password reset
    this.oauthService.loginUrl = this.oauthService.loginUrl.replace(
        'b2c_1_signupandsignin',
        'b2c_1_passwordreset'
    );

    this.oauthService.initCodeFlow();
}

private userHasRequestedPasswordReset(err: OAuthErrorEvent): boolean {
    return (err.params['error_description'] as string).startsWith(
      'AADB2C90118'
    );
}
Enter fullscreen mode Exit fullscreen mode

This will make sure a user gets directed back to Azure and into the correct flow. If a user now resets their password, they get directed back to your app with the code and our app can fetch the access token and id token.

But our app breaks. :'(

I will leave out most of the gory details, but what happens is that our app "sees" the code coming in and starts the code exchange part of the flow (see step 3 in the diagram above). It does that using the default AuthConfig and performs a POST to the default/configured 'b2c_1_signupandsignin' profile endpoint. But our code challenge was done on the 'b2c_1_passwordreset' endpoint and thus Azure throws a "HTTP4xx you screwed up" error. To fix that, we need to make sure that in the case of reset-password, we override the profile on the token endpoint (like we did on the authorize endpoint earlier). This is not that difficult, because we can send a "state" along with our requests. On the way back we will pick up this state and if it is present, we fix the token endpoint:

this.oauthService
  .loadDiscoveryDocument(url)
  .then((_) => {
    if (this.userHasEnteredPasswordResetFlow()) {
      // We need to change to token endpoint to match the reset-password flow
      this.oauthService.tokenEndpoint.replace(
        'b2c_1_signupandsignin',
        'b2c_1_passwordreset'
      );
    }

    return this.oauthService.tryLoginCodeFlow();
  })
  .then((_) => {
    if (!this.oauthService.hasValidAccessToken()) {
      this.oauthService.initCodeFlow();
    }
  })
  .catch((err) => {
    if (this.userHasRequestedPasswordReset(err)) {
      // In this case we need to enter a different flow on the Azure AD B2C side.
      // This is still a valid Code + PKCE flow, but uses a different form to support self service password reset
      this.oauthService.loginUrl = this.oauthService.loginUrl.replace(
        'b2c_1_signupandsignin',
        'b2c_1_passwordreset'
      );
      // Add this to the state as we need it on our way back
      this.oauthService.initCodeFlow('PASSWORD_RESET');
    } else {
      // Another error has occurred, e.g. the user cancelled the reset-password flow.
      // In that case, simply retry the login.
      this.oauthService.initCodeFlow();
    }
  });

  private userHasEnteredPasswordResetFlow(): boolean {
    return window.location.search.indexOf('PASSWORD_RESET') > -1;
  }

  private userHasRequestedPasswordReset(err: OAuthErrorEvent): boolean {
    return (err.params['error_description'] as string).startsWith(
      'AADB2C90118'
    );
  }
Enter fullscreen mode Exit fullscreen mode

You can find a fully working example app here (just update the config): https://github.com/yuriburger/ng-azureb2c-pkce-demo

Thanks Daan Stolp for working with me on the Azure tweaks!

/Y.

More info:

Top comments (0)