6 minute read

While working on a project, I stumbled upon an interesting issue - how to force the user to reauthenticate in an application - for example when accessing some sensitive information? While it may seem quite straightforward from the documentation of Azure AD, it is not that simple, and if you are using prompt=login to reauthenticate the user, I quite suggest you read on.

prompt=login and why it is bad

So when I started solving the issue, I looked into the Authorization Flow documentation and found the following: when you add a prompt=login into the authorization URL, will make the user reauthenticate - so I assumed: Hey! This is exactly what I need, let's use it.

However after implementing this (very simple and straightforward), I thought, let's try to be a bad user and avoid reauthentication! So when the user got forwarded to the authorization URL and prompted for their password, I removed the prompt=login from the URL, refreshed the page and believe it or not, I was signed into the application and seen the "sensitive" information!

It was time to dig a bit deeper into this on the token level. So after logging in with prompt=login and without it, I discovered that the tokens are basically the same. So despites the user entering their credentials, there was no way to actually authenticate whether they really did it. So while visually this did what you wanted the regular user to see, on the background, there were no measures to detect what happened.

Luckily, I found solution quite quickly...

Doing it the right way...

So after few desperate Google searches, I took a look into the OIDC RFC and found out, that you can append max_age=<time since password was last entered> to the authorization URL (or just put 0 to force password authentication at all times). So once user gets redirected to the URL, they will be presented with an information to login again:

Now this looked better then just the screen with prompt=login which shown plain login screen without account selection and information about what happened which is quite important from the user prospective.

But this was just cosmetic, does it let us distinguish the situation on the backend? Yes! When the user is returned after authentication to your application, the id_token is going to contain a claim called auth_time. This claim holds the Unix timestamp of when the user entered the password last. The last thing to do was to validate this information.

Validating auth_time

Validation is quite simple, the RFC, specifies it like this: check the auth_time Claim value and request re-authentication if it determines too much time has elapsed since the last End-User authentication.

Which I believe is quite clear explanation of how to do this.

Pratical use with ASP.NET Core

For the last part, I am going to include few code samples of how to achieve this in ASP.NET Core with OpenIdConnect middleware:

Forcing authentication

First, you are going to have to modify the OpenIdConnectEvents in order to be able to redirect the user to the correct URL. This is done by modifying context of OnRedirectToIdentityProvider:

// ...
OnRedirectToIdentityProvider = context =>
{
    if (context.ShouldReauthenticate())
    {
        context.ProtocolMessage.MaxAge = "0"; // <time since last authentication or 0>;
    }
    return Task.FromResult(0);
},
// ...

ShouldReauthenticate is an extension method of RedirectContext, which decides (based on current state, which we will set later) whether the user should reauthenticate or not:

internal static bool ShouldReauthenticate(this RedirectContext context)
{
    context.Properties.Items.TryGetValue("reauthenticate", out string reauthenticate);

    bool shouldReauthenticate = false;
    if (reauthenticate != null && !bool.TryParse(reauthenticate, out shouldReauthenticate))
    {
        throw new InvalidOperationException($"'{reauthenticate}' is an invalid boolean value");
    }

    return shouldReauthenticate;
}

Next, you can test this by calling ChallengeAsync in your controller. Before that, you have to pass it a state information for the user to be reathenticated.

var state = new Dictionary<string, string> { { "reauthenticate", "true" } };
await HttpContext.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state)
{
    RedirectUri = "/sensitivePage"
}, ChallengeBehavior.Unauthorized);

Note that we are also passing ChallengeBehavior.Unauthorized there, which results in the request not failing with Forbidden, but allows it to proceed (this took me a while to figure out, solution found on GitHub).

After this, you have successfully set up the redirect along with the reauthentication enforcement. Next up is the token validation, which is very important.

id_token validation

We are going to validate the id_token's auth_time claim within the specific controller, which in my opinion makes the most sense. We are going to achieve that by implementing Attribute and IResourceFilter, to create our own attribute filter.

public class RequireReauthenticationAttribute : Attribute, IAsyncResourceFilter
{
    private int _timeElapsedSinceLast;
    public RequireReauthenticationAttribute(int timeElapsedSinceLast)
    {
        _timeElapsedSinceLast = timeElapsedSinceLast;
    }
    public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        var foundAuthTime = int.TryParse(context.HttpContext.User.FindFirst(AppClaimTypes.AuthTime)?.Value, out int authTime);

        if (foundAuthTime && DateTime.UtcNow.ToUnixTimestamp() - authTime < _timeElapsedSinceLast)
        {
            await next();
        }
        else
        {
            var state = new Dictionary<string, string> { { "reauthenticate", "true" } };
            await context.HttpContext.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state)
            {
                RedirectUri = context.HttpContext.Request.Path
            }, ChallengeBehavior.Unauthorized);
        }
    }
}

So now we can is it in our controller as easy as putting [RequireReauthentication(<maximum time elapsed since last authentication>)] above a method or entire controller class.

This code also works great with slight modifications with DotVVM framework.

And this is it. Now we have reauthentication implemented correctly while making sure that the reauthentication has really happened.

Requiring Multi Factor Authentication

Just additional update: When you want to require the user to use MFA for login session, you can modify the code above and instead of checking the authentication time you will be check for authentication method reference in the token. If it contains mfa it means that user has used Multi Factor Authentication for this session, additionally if it contains pwd it also means the user authenticated using their password.

In order to force MFA to be used, you have to append amr_values=mfa to the authorization URL for the user. To do this with OpenIdConnectMiddleware in ASP.NET Core, you have to do following in place for setting MaxAuth:

context.ProtocolMessage.SetParameter("amr_values", "mfa");