简体   繁体   中英

How to refresh Azure AD B2C identity token

I'm using Azure AD B2C with OpenIdConnect for authentication to a web application. I've got it mainly working except that the authentication times out after an hour, even if the user is actively using the application.

It is an old webapp built mostly with ASPX pages. I'm just using an identity token and I'm using cookies. I am not using an access token at all in my app. Access is done a pre-existing way, based on the user claims. I'm using the MSAL.Net library in Microsoft.Identity.Client. Logging in works fine. I get a code back which then gets exchanged for an identity token

AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(Globals.Scopes, notification.Code).ExecuteAsync();

Everything was working fine, except that the token would expire after 1 hour, no matter what I did. Even if I was using the app, the first request after an hour would be unauthenticated. I tried adding a call to silently acquire a token to see if that would refresh it, but it did not. With OpenIdConnect the offline_access scope is always included. If I try to include it explicitly it throws an error saying so. But I've never seen any evidence that there is a refresh token, even behind the scenes.

I found this question on StackOverflow - Azure AD B2C OpenID Connect Refresh token - and the first answer referenced an OpenIdConnect property called UseTokenLifetime. If I set that to false, then I wouldn't lose authentication after an hour, but the now it was too far the other way. It seemed like the token/cookie would never expire, and I could stay logged in forever.

My desire is that as long as the user is actively using the application, they stay logged in, but if they stop using it for some time (an hour), they have to re-authenticate. I found a way to make that happen through hours of trial and error, I'm just not sure if it makes sense and/or is secure. What I'm doing now is that on each authenticated request, I update the "exp" claim of the user (not sure this matters), and then generate a new AuthenticationResponseGrant, setting the ExpiresUtc to the new time. In my testing, if I hit this code in less than an hour, it keeps me logged in, and then if I wait beyond an hour, I'm no longer authenticated.

HttpContext.Current.User.SetExpirationClaim(DateTime.Now.AddMinutes(60.0));

public static void SetExpirationClaim(this IPrincipal currentPrincipal, DateTime expiration)
{
    System.Diagnostics.Debug.WriteLine("Setting claims expiration to {0}", expiration);
    int seconds = (int)expiration.Subtract(epoch).TotalSeconds;
    currentPrincipal.AddUpdateClaim("exp", seconds.ToString(), expiration);
}

public static void AddUpdateClaim(this IPrincipal currentPrincipal, string key, string value, DateTime expiration)
    {
        var identity = currentPrincipal.Identity as ClaimsIdentity;
        if (identity == null)
            return;

        // check for existing claim and remove it
        var existingClaim = identity.FindFirst(key);
        if (existingClaim != null)
            identity.RemoveClaim(existingClaim);

        // add new claim
        identity.AddClaim(new Claim(key, value));
        var authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
        authenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(new ClaimsPrincipal(identity),
            new AuthenticationProperties() {
                IsPersistent = true,
                ExpiresUtc = new DateTimeOffset(expiration).UtcDateTime,
                IssuedUtc = new DateTimeOffset(DateTime.Now).UtcDateTime
            });
    }

My question is, does this make sense? Is there any downside? I've never seen any suggestions to do it this way, but it was the only thing I found that worked. If there is a better way to do it, I'd like to know what it is. I considered making my current code an "answer" instead of including it in the question, but I'm not confident that it is correct.

To refresh ID token, you need to use refresh token. Refresh token is opaque to client, but could be cached by MSAL. Then when ID token is expired, MSAL will use the cached refresh token to get a new ID token.

However, you need to implement the cache logic by yourself like instructed in official sample .

Core code snipet:

                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        RedirectToIdentityProvider = OnRedirectToIdentityProvider,
                        AuthorizationCodeReceived = OnAuthorizationCodeReceived,
                        AuthenticationFailed = OnAuthenticationFailed,
                    },
    private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
    {
        try
        {
            /*
             The `MSALPerUserMemoryTokenCache` is created and hooked in the `UserTokenCache` used by `IConfidentialClientApplication`.
             At this point, if you inspect `ClaimsPrinciple.Current` you will notice that the Identity is still unauthenticated and it has no claims,
             but `MSALPerUserMemoryTokenCache` needs the claims to work properly. Because of this sync problem, we are using the constructor that
             receives `ClaimsPrincipal` as argument and we are getting the claims from the object `AuthorizationCodeReceivedNotification context`.
             This object contains the property `AuthenticationTicket.Identity`, which is a `ClaimsIdentity`, created from the token received from
             Azure AD and has a full set of claims.
             */
            IConfidentialClientApplication confidentialClient = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(notification.AuthenticationTicket.Identity));

            // Upon successful sign in, get & cache a token using MSAL
            AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(Globals.Scopes, notification.Code).ExecuteAsync();
        }
        catch (Exception ex)
        {
            throw new HttpResponseException(new HttpResponseMessage
            {
                StatusCode = HttpStatusCode.BadRequest,
                ReasonPhrase = $"Unable to get authorization code {ex.Message}."
            });
        }
    }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM