Azure AD B2C: How to revoke refresh tokens?

Welcome back to the… but first:

That was quite a break since my last post here… But enough of my laziness, it’s time to resume the Azure AD B2C series (or should I already call it Azure AD External Identities maybe?), the rest of the blog and my other community activities which died out recently.

So, welcome back to the AAD B2C series!

Introduction

While using OAuth you sooner or later encounter a refresh token which allows retrieving new access token for application without any user interaction. However, as you add new features, applications, etc. and your solution grows you might meet with a challenge of invalidating those tokens. This posts covers different scenarios and options you have to do that in Azure AD B2C service.

Use cases – why do you revoke refresh tokens at all?

There may be different reasons for token invalidation all of them have an essential target though – new authentication is required afterwards. However, because reasons are different also the solutions to the problem might need to be different to cover specific requirements or provide required behaviours. I will focus on a two cases you might encounter while working with Azure AD B2C which have distinct problems to solve:

A. Global sign out

This is a scenario, where user wants to sign our from every device, mobile app, web page, whatever you name it. Just everywhere. Some applications, which don’t make use of refresh tokens might not pose a problem – they will just loose any access to user’s resources when access tokens expire. However, there might be services which act on behalf of user and retrieve user access token offline (without user interaction) by using a refresh token.

You might want your users to have an option to globally revoke all those tokens in various circumstances, most common would probably be a suspicion of a breach and leaked credentials or leaked refresh tokens, etc. You then want to revoke any token the user or anyone else, using user’s credentials, might have retrieved.

B. New access token requirements

After refresh token is retrieved from AAD B2C it can be used to get new access tokens. This refreshing however has a downside – it doesn’t refresh everything as you might expect. What it does is it issues a new access token, with new expiration date but with the same claim bag as the initial token. In other words – claim values in the token do not change even if their source was updated. So when the user has a token issued with the “John Smith” displayName value the refresh token retrieved in this same flow will keep issuing tokens with “John Smith” claim value even though the user might have updated this value to ex. “Johnny B. Smith” in the meantime.

This behaviour might stop you from introducing new features where you want more data in the token or when you just badly need data in the token up to date with data sources. In this scenario you need the user to reauthenticate to issue new tokens, with up to date information. One of the ways to prevent application from using “old” refresh token and force reauthentication is to block the use of the refresh token the application holds.

Global (per user) revocation

This is the option you might want to use when you are solving case A. or any other problem which involves revoking all user refresh tokens.

It is achieved by either calling Microsoft Graph API

or by using Powershell

Both methods revoke ALL refresh tokens issued before the moment of execution of the API call or Powershell command.

What both of them do is update a refreshTokensValidFromDateTime field in user object in the directory – set it to current DateTime.

They also work for both policy types – built-in or custom – as they are user-centric so it’s valid for every flow which this user interacts with (or tokens of this user are used with).
This user-centricity also requires you to run it separately for every user you require so ex. if you suspect a breach where the subset of users might have been exposed and need their tokens revoked you need to do it for every such user.

Custom (per policy) revocation

Important: works for custom policies ONLY!

If you dig deep enough in custom policies you might discover a way to work with refresh tokens differently than the previous option allows.

The whole secret lies in the JwtIssuer technical profile which is probably in the end of every UserJourney you encountered. Let’s take a closer look at what can be found in the definition of this profile in Base policy (some parts removed for simplicity):

What is most interesting here is the RefreshTokenUserJourneyId metadata value, let’s find that too:

This is the first important takeaway – when the refresh_token grant is executed this UserJourney runs. If so, then we are in position to change the behaviour by creating own, custom UserJourney.

A proper question to ask at this point is – how this RedeemRefreshTokenV1 works, what does it do? – to know how to prepare a custom one. Let’s get down to business again then. As you are probably aware – refresh token you get from AAD B2C is opaque, there is no way to determine for which user this token was issued for. Well, that’s what the TpEngine_RefreshTokenReadAndSetup technical profile is for:

It simply returns objectId, being a user id in this case, and the refreshTokenIssuedOnDateTime is a DateTime token was issued at. This data is used by the technical profile used in the next step of the UserJourney:

The objectId from the previous step is used to run AAD-UserReadUsingObjectId part on which this profile is based – read user data. What it adds on top of it is specifying an additional output of the reading procedure- the DateTime value before which tokens are deemed invalid (see Global revocation part) and a claim transformation:

This transformation is an Assert which verifies the relation between the time the token was issued at and the time saved in user object as “refresh tokens valid only if issued after that”. When the token was issued after this date everything is fine, if not, the whole flow fails with an exception and the result is no tokens are issued.

So, another takeaway – this UserJourney is responsible for the behaviour I described in Global revocation part. By default it only compares two DateTimes but with a UserJourney of our own we can do really a lot, even call a REST API to provide information from systems outside of AAD B2C.

Example

What exactly can you use it for? A simple example might be to verify if the user has a specific property set in the user object. Let’s say – country of residence. There is a ClaimType already defined to store and retrieve it from the directory – cpiminternal_legalCountry and some other objects to check its existence – isLegalCountryPresent boolean claim type and IsLegalCountryPresent claim transformation. You will also need new Assert transformation:

and then a technical profile which will read the cpiminternal_legalCountry value from user object, run IsLegalCountryPresent transformation and the newly created Assert one:

Then it’s enough to copy the RedeemRefreshTokenV1 user journey (to keep the initial DateTime comparison feature) and do a small adjustment by inserting a new OrchestrationStep (here – nr 3):

I did it with a separate technical profile and a separate orchestration step just for sake of example to have the feature cleanly separated but it can be also accomplished by building the profile on top of AAD-UserReadUsingObjectId-CheckRefreshTokenDate and making a change only in OrchestrationStep 2.

The final touch is a new technical profile based on JwtIssuer but overriding the user journey responsible for refresh_token grant:

Now it’s enough to use the new technical profile to issue token from every policy you wish to incorporate the new behaviour. This way you don’t have to use the global revocation but keep user’s refresh tokens where the new requirement doesn’t apply and block it from being used without country of residence set only for selected policies.

Summary

I hope you liked that entry. It was quite fun for me discovering how it works and doing changes for my project. Maybe you fill find it useful someday too.

Coming up – custom sign in with undisclosed list of supported external providers and provider detection (probably… :))

4 thoughts on “Azure AD B2C: How to revoke refresh tokens?”

  1. Hi Wojtek,
    It was really a nice article.

    I was following this article to invalidate the b2c refresh token. I’ve generated the access token using ROPC flow. When I tried to invalidate the refresh token using the API you mentioned, I’m getting “InvalidAuthenticationToken” with the message “Invalid x5t claim.”.

    Below is the request which I’m calling –
    POST-https://graph.microsoft.com/1.6/users/{user_object_id}/invalidateAllRefreshTokens
    HEADER- Authorization:Bearer

    I tried using 2.0 as a version as well but same error.

    I’m stuck in this for few days. Please help me out.
    Thanks in advance!

  2. Hi! Thanks for a great article. However I’ve faced one issue with invalidateAllRefreshTokens. I have a SPA with MSAL.js v2 + B2C based authentication. When I call the endpoint above, nothing’s happened on FE. I am still able to silently refresh my access token. Looks like it doesn’t affect refresh token at all.
    Do you know if it’s expected behavior or not?

    And Q2: do you where can I take Identity Experience Framework XMLs for existing base user flows? I see a difference between B2C starter pack and built-in user flows. For instance: when I turn MFA on, my custom policy based on starter pack is asking to enter a phone number every time. However the standard user flow only asks it first time and then it sends a verification code SMS immediately.

    1. You mean not issuing them at all from Identity Service? In general it’s enough to not request offline_access scope. However, how do you disable it probably depends on implementation of the SDK/library/whatever you’re using.

Leave a Reply to Amar Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.