Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access Token renewal on .Net Framework 4.8 #1555

Open
RtypeStudios opened this issue Jan 22, 2025 · 2 comments
Open

Access Token renewal on .Net Framework 4.8 #1555

RtypeStudios opened this issue Jan 22, 2025 · 2 comments

Comments

@RtypeStudios
Copy link

RtypeStudios commented Jan 22, 2025

Which version of Duende.AccessTokenManagement are you using?
We are currently not. We are using .Net Framework, which the library doesn't support. The code for review below achieves a similar goal, hence why I logged it under this.

Which version of .NET are you using?
.Net Framework 4.8

This isn't a bug more of a question / code review
We have a .Net Framework 4.8 application (APP) which authentications to Identity server 6.10.3, which works perfectly. This application then makes calls to another application (also authenticated via the same SSO) via an API call (API).

Over time, the access token needs to be refreshed in the APP to be able to continue talking to the API. This requires a refreshing of the token periodically. Which there doesn't seem to be a pre built component for.

Below is my implementation, which works correctly, but being a bit paranoid about security. I thought it might be wise to have the experts run an eye over it and to share it with other developers who might be in the same situation.

The code below fetches the access token from OWIN, then checks the expiry, if the token is expired. The system then requests a new token and updates the users authentication cookie (with updated access_token and refresh token). This caches the updated tokens for future requests. If the token hasn't expired, it is returned directly.

As the experts, can you see any issues with the below?

/// <summary>
/// Get and optionally refresh the JWT access token for the currently logged-in user provided by the SSO system.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/> to get the token from.</param>
/// <returns>A string containing the JWT token.</returns>
public static async Task<string> GetAccessToken(this HttpContext context)
{
    if (context == null) throw new ArgumentNullException(nameof(context));

    var authenticateResult = await context.GetOwinContext().Authentication.AuthenticateAsync("cookie").ConfigureAwait(false);

    if (authenticateResult?.Properties == null || authenticateResult.Identity == null)
    {
        throw new InvalidOperationException("Authentication result or its properties are null.");
    }

    if (!authenticateResult.Properties.Dictionary.TryGetValue("access_token", out var token) || string.IsNullOrEmpty(token))
    {
        throw new InvalidOperationException("Access token is missing.");
    }

    var tokenHandler = new JwtSecurityTokenHandler();
    var decodedToken = tokenHandler.ReadJwtToken(token);

    // Do we need to refresh the token, if not return it.
    if (decodedToken.ValidTo >= DateTime.UtcNow) { return token; }

    // Otherwise, renew the token.
    var ssoAuthority = ConfigurationManager.AppSettings["SsoAuthority"];
    var ssoClientId = ConfigurationManager.AppSettings["SsoClientId"];
    var ssoSecret = ConfigurationManager.AppSettings["SsoSecret"];

    if (string.IsNullOrEmpty(ssoAuthority) || string.IsNullOrEmpty(ssoClientId) || string.IsNullOrEmpty(ssoSecret))
    {
        throw new InvalidOperationException("SSO configuration is missing or invalid.");
    }

    using (var client = new HttpClient())
    {
        try
        {
            // Fetch configuration from the SSO.
            var disco = await client.GetDiscoveryDocumentAsync(ssoAuthority).ConfigureAwait(false);
            if (disco.IsError) { throw new InvalidOperationException($"Error fetching discovery document from {ssoAuthority}."); }

            using (var tokenRefreshRequest = new RefreshTokenRequest())
            {
                // Create token request.
                tokenRefreshRequest.Address = disco.TokenEndpoint;
                tokenRefreshRequest.ClientId = ssoClientId;
                tokenRefreshRequest.ClientSecret = ssoSecret;
                tokenRefreshRequest.RefreshToken = authenticateResult.Properties.Dictionary["refresh_token"];

                // Get refreshed tokens from the SSO.
                var tokenRefreshResponse = await client.RequestRefreshTokenAsync(tokenRefreshRequest).ConfigureAwait(false);
                if (tokenRefreshResponse.IsError) { throw new InvalidOperationException("Error refreshing token."); }

                // Copy the claims, without the access_token and refresh_token.
                var updatedClaims = authenticateResult.Identity.Claims
                    .Where(t => t.Type != "access_token" && t.Type != "refresh_token")
                    .ToList();

                // Add updated token back in.
                updatedClaims.Add(new System.Security.Claims.Claim("access_token", tokenRefreshResponse.AccessToken));
                updatedClaims.Add(new System.Security.Claims.Claim("refresh_token", tokenRefreshResponse.RefreshToken));

                // Create new Identity.
                var updatedIdentity = new ClaimsIdentity(updatedClaims, authenticateResult.Identity.AuthenticationType);

                // Update the token in the properties.
                var updateProperties = authenticateResult.Properties;
                updateProperties.Dictionary["access_token"] = tokenRefreshResponse.AccessToken;
                updateProperties.Dictionary["refresh_token"] = tokenRefreshResponse.RefreshToken;

                // Update the auth with new access details.
                context.GetOwinContext().Authentication.SignIn(updateProperties, updatedIdentity);
                     
                // Assign the updated token for return.
                token = tokenRefreshResponse.AccessToken;
            }
        }
        catch (Exception ex)
        {
            // Log the exception (consider using a logging framework)
            throw new InvalidOperationException("An error occurred while refreshing the token.", ex);
        }
    }

    return token;
 }
@RolandGuijt
Copy link

I think this might not be the right place to ask this since it's not about any of our products and we can't accept any responsibility for the correctness of your code nor support it, but I can can give you a few hints:

  • The client (where this code is running) isn't allowed to read the access token. It is just for the API. The request coming back from the identity provider that contains the access token has a JSON structure that contains the expiration information. Use that instead.
  • It seems like access and refresh token are added both to the ClaimsIdentity and the authentication properties.

@RtypeStudios
Copy link
Author

Thanks Roland for the advice, Yes, I noticed the second one late yesterday.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants