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");

Comments

Oj

This is a really helpful article and was the only one i could find to address the reauthentication requirement. However I have had to make some adjustments for my asp net core 2.0 mvc web app which is kind of working (after adding options.ClaimActions.Remove(“auth_time”) recommended from your other post) but it’s not quite robust yet. Just wondering if you can advise on the following…

I had to use the line “await AuthenticationHttpContextExtensions.ChallengeAsync(context.HttpContext, OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties(state));” in order for it to compile. Also I could not find an option for “ChallengeBehavior.Unauthorized” which you specifically mention however I assume this gives the same result but do you know? Adding the resource filter decoration “[RequireReauthentication(0)]” to my controller action works however passing the value 0 means the code never reaches the await.next() command. What should this value be? I had to use “DateTimeOffset.UtcNow.ToUnixTimeSeconds()” as there is no “DateTime.UtcNow.ToUnixTimestamp()” option in asp net core 2.0. I assume this gives the same result but would be nice to confirm. When the login form appears it is the same as the initial full page login page when first opening the app. Is this normal as I was expecting a more trimmed down popup window. Is there any way to customise the options on this form (ie from the ProtocolMessage) to remove other account selections and provide a way for a user to cancel which gracefully returns to the app in it’s current location? There does not seem to be a way a user can get out of the login screen without closing the browser window. It works when calling a controller action that return a full page view. However when I call an action from an ajax request which returns a partial into a bootstrap modal it fails with “Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘https://localhost:44308’ is therefore not allowed access”. This looks like a CORS issue but I don’t know why it would work when going through the standard mvc process and not when being called from jquery. Adding “services.AddCors();” and “app.UseCors(builder => builder.WithOrigins(“https://login.microsoftonline.com”));” to my startup file doesn’t make any difference. Do you know what could be the issue here?

Thanks for taking the time to put this article together and any responses you can provide.

Jan Hajek

So first off, this article was made during ASP.NET Core 1.0 / 1.1 era, so it is sort of outdated. You did the right thing with the ClaimsActions and are on right track. The ChallengeBehavior was removed in ASP.NET Core 2.0. The ToUnixTimestamp method is an extension method I made and looks like this (an official one might be added in .NET Core 2.1):

public static class DateTimeExtensions
{
    public static long ToUnixTimestamp(this DateTime d)
    {
        var duration = d - new DateTime(1970, 1, 1, 0, 0, 0);
        return (long)duration.TotalSeconds;
    }
}

The proper login screen when reauthentication is prompted looks like this (note the “Because you are accessing…” message): Well with AJAX it is a bit more complicated - what you should do, is that when you hit your AJAX route (“/ajax/…” for example) and reauthentication is required, you should return back a status code like 401 with JSON response like ({“status”:”reauthentication_required”,…}) and then process the message on client side and then have it prompt reauthentication. The reauthentication will not work with AJAX to my knowledge.

AAB

It will be great if you can suggest something on the same front for an older application i.,e ASP.NET MVC and not core specific. Or if you can recommend some links to follow which can resolve issue for me.

Norm

login_hint came close to force user to re-enter credentials. However, browser profile sync just automatically bypasses upon account selection it the user has setup (similar to the save userid password when visiting many websites). With the parameters option, why not a full querystring encryption share between the provider and application to prevent the user from manipulating or removing the values?

Dagur

Hey Jan, Thanks, a really insightful post, thanks. Do you have any thoughts on how this could be applied to a subset of users within Azure AD? IE user group A we do NOT apply “max_age=

To submit comments, go to GitHub Discussions.