okta / okta-aspnet

okta-aspnet
https://github.com/okta/okta-aspnet
Apache License 2.0
86 stars 52 forks source link

Support for Refresh Tokens #130

Closed Good-man closed 4 years ago

Good-man commented 4 years ago

Hi @chrismorris-okta

User story

As a website developer, I would like the Okta ASP.net SDK to support the flow of exchanging a refresh token for a new access token.

Our use-case is a feature that we call "keep me logged in" where our customers can continue to access authenticated, but non-sensitive resources when they return to the site for weeks or months (similar to Amazon and Facebook). Accessing sensitive resources, like personal information, will be handled by the application and will challenge the user to reauthenticate.

Proposed solution

The application developer will store the user's refresh and id token somewhere, perhaps a cookie, and trigger a OIDC challenge containing these as authentication properties. The Okta SDK middleware would then issue a call to the /token (instead of the /authorize) endpoint to request a new access token. If successful, the middle ware will set the user to authenticated and continue.

Alternatives considered

An alternative is to add an entirely separate layer to the request pipeline that does all of this work and bipasses the OIDC authentication layer.

Additional information

We are using classic ASP.net MVC with Owin. Core is not an option because our CMS doesn't currently support it.

chrismorris-okta commented 4 years ago

@Good-man thanks for submitting this issue. This isn't supported in the SDK today but our team is actively looking into how we can officially support this in the SDK and if there is a workaround we can suggest.

We'll get back to you in a couple of weeks once we have more information.

Good-man commented 4 years ago

This isn't supported in the SDK today but our team is actively looking into how we can officially support this in the SDK and if there is a workaround we can suggest.

Totally understand! I would love a suggested workaround! Thank you!

Good-man commented 4 years ago

Hi @chrismorris-okta,

I am able to retrieve a new Access Token using the Refresh Token, but where in the Okta/Owin/OIDC pipeline do I reintroduce the new tokens?

Mark

Good-man commented 4 years ago

I posted a related issue on StackOverflow.

How does a client using Owin/Katana/OIDC use a Refresh Token?

chrismorris-okta commented 4 years ago

Thanks for the follow-up Mark. I'm tagging @laura-rodriguez to help answer this since she's digging into this area this week

laura-rodriguez commented 4 years ago

Hi @Good-man,

Thanks for your question.

The user session lifetime is separate from access tokens' lifetime. Access tokens are usually used by a resource server to authorize a request. The OIDC SDK is responsible for the authentication and retrieving of the tokens, but then it is up to the client (your app) to decide how they manage their sessions or authorize resources.

That being said, based on your comment Our use-case is a feature that we call "keep me logged in" where our customers can continue to access authenticated, but non-sensitive resources when they return to the site for weeks or months (similar to Amazon and Facebook)., it seems your session is already independent of the access token's lifetime. Is that correct? If so, maybe you can write your own Authorization filter that does the token verification for you (tokens are stored as user claims), and apply these to your protected endpoints, something like this:

I haven't tried this by myself, but maybe this could be an option to explore.

Are your protected endpoints part of your MVC application? Or are they in a different Web API project? Do you have any code to share with us to see what option you are currently exploring and where are you getting stuck?

Good-man commented 4 years ago

Hi @laura-rodriguez ,

Yes, user session is completely independent of the access token's lifetime. No problem there.

I have already successfully implemented a check for token expiration and a call to /token to retrieve new tokens, however I have been unable to trigger the CookieAuthentication middleware to set a new cookie with the new access token.

This SO question describes the problem better than I did: How do I update my cookie, having got a new access_token?.

The answer suggested this:

SecurityTokenValidated = context =>
                        {
                            context.AuthenticationTicket.Properties.AllowRefresh = true;
                            context.AuthenticationTicket.Properties.IsPersistent = true;
                        }

And this:

HttpContext.Current.GetOwinContext().Authentication.SignIn(new AuthenticationProperties
                                    {
                                        ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn),
                                        AllowRefresh = true,
                                        IssuedUtc = DateTime.UtcNow,
                                        IsPersistent = true
                                    }, newIdentity);

On every request and within custom Owin middleware, I do this:

if (CheckAccessToken(context))
{
    await RefreshTokens(context);
}

And this:

private bool CheckAccessToken(IOwinContext context)
{
    var id = (ClaimsIdentity)context.Authentication.User.Identity;

    var accessTokenString = id.FindFirst(ClaimTypeKey.AccessToken)?.Value;
    if (accessTokenString == null)
        return false;
    var accessToken = new JwtSecurityTokenHandler().ReadToken(accessTokenString);

    var oneMinuteBeforeExpiration = accessToken.ValidTo.AddMinutes(-1);  // todo: this should be configurab
    var needsRefreshed = oneMinuteBeforeExpiration <= DateTime.UtcNow;

    return needsRefreshed;
}

private async Task RefreshTokens(IOwinContext context)
{
    var id = (ClaimsIdentity)context.Authentication.User.Identity;

    var refreshToken = id.FindFirst(ClaimTypeKey.RefreshToken)?.Value;
    if (refreshToken != null)
    {
        var tokenManager = new TokenManager(_options.Issuer, _options.ClientId, _options.ClientSecret);
        var tokenResponse = await tokenManager.RequestNewTokens(refreshToken);

        // todo: add exception handling 
        var newAccessToken = new JwtSecurityTokenHandler().ReadToken(tokenResponse.access_token);

        var result = from claim in id.Claims
                     where claim.Type != ClaimTypeKey.AccessToken
                         && claim.Type != ClaimTypeKey.RefreshToken
                         && claim.Type != ClaimTypeKey.IdToken
                     select claim;
        var claims = result.ToList();

        claims.AddRange(new Claim[]
        {
            new Claim(ClaimTypeKey.AccessToken, tokenResponse.access_token),
            new Claim(ClaimTypeKey.IdToken, tokenResponse.id_token),
            new Claim(ClaimTypeKey.RefreshToken, tokenResponse.refresh_token),
        });

        var newIdentity = new ClaimsIdentity(claims, _options.AuthenticationType);
        DateTimeOffset expiresUtc = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.expires_in);
        context.Authentication.SignIn(new Microsoft.Owin.Security.AuthenticationProperties()
        {
            ExpiresUtc = expiresUtc,
            AllowRefresh = true,
            IssuedUtc = DateTime.UtcNow,
            IsPersistent = true
        }, newIdentity);
    }
}

But this still doesn't trigger the CookieAuthentication middleware to set a new cookie, so subsequent request still contain the old Access Token. :-(

Mark

laura-rodriguez commented 4 years ago

Thanks for all the details Mark!

Just to clarify, when you say that a new cookie is not being generated, do you mean that context.Authentication.User.Identity still has the old tokens? In other words, context.Authentication.User.Identity is not being overwritten with the newIdentity. Is the ExpiresUtc being updated?

Have you tried Challenge instead of SignIn? You can do a quick test to see if the ExpiresUtc property is being updated.

HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties()
                {
                    IsPersistent = true,
                    ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(5),
                    ...
                })

Can you also post your Startup class configuration, please? That will help us to see the whole project configuration :).

Good-man commented 4 years ago

Hi @laura-rodriguez,

Correct. After passing newIdentity to the SignIn() method, the middleware does not set a new cookie so it contains all of the old claims and tokens. So, on the next request, the middleware parses the old cookie and restores all of the old claims and tokens.

The Challenge() method does not accept a ClaimsIdentity argument like the SignIn() method does, however....

I tried Challenge() a few weeks ago and included the refresh_token in the dictionary similar to how we set a sessionToken property after the call to /authn, and this DOES trigger a browser redirect to Okta which obtains new tokens! At first, I thought that this was going to work, but unfortunately this only works if there is already an active Okta session cookie. If I close and reopen my browser (which clears the Okta session cookie), Okta redirects the browser to an Okta sign in page instead of my app's custom login page, does not use the refresh token. :-(

Below is my Startup.Configuration(). You may notice that .UseCeAuthMvc() looks a lot like Okta's .UseOktaMvc(). That is no coincidence. It was suggested by Okta's Chris Gustafson that I implement using built-in Microsoft middleware so that I could customize where I need to for our use-case. Most of it is the same, but I did customize in a few places.

public void Configuration(IAppBuilder app)
{
    app.UseDebugMiddleware(new DebugMiddlewareOptions
            {
                OnIncomingRequest = (ctx) =>
                {
                    var watch = new Stopwatch();
                    watch.Start();
                    ctx.Environment["DebugStopwatch"] = watch;
                },
                OnOutgoingRequest = (ctx) =>
                {
                    var watch = (Stopwatch)ctx.Environment["DebugStopwatch"];
                    watch.Stop();
                    Debug.WriteLine("Request took: " + watch.ElapsedMilliseconds);
                }
            });
    app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        //CookieName = ".AspNet.Cookies",  // this is the default
        LoginPath = new PathString("/Account/Login"),
        // This is critical!  See https://github.com/aspnet/AspNetKatana/wiki/System.Web-response-cookie-integration-issues
        CookieManager = new SystemWebCookieManager(),
        //CookieSameSite = Microsoft.Owin.SameSiteMode.Strict, // Cannot be set to Strict!
        // Note: OpenIdConnectAuthenticationOptions.UseTokenLifetime must be false for ExpireTimeSpan and SlidingExpiration to be honored
        ExpireTimeSpan = Settings.RememberMeTimeSpan, 
        SlidingExpiration = true,
        Provider = new CeCookieAuthenticationProvider(),
    });
    //var issuerUrl = CeAuthUrlHelper.CreateIssuerUrl(Settings.OktaDomain, Settings.AuthorizationServerId);
    //app.UseRefreshToken(new RefreshTokenMiddlewareOptions(issuerUrl, Settings.ClientId, Settings.ClientSecret));
    app.UseCeAuthMvc(new CeAuthOptions
    {
        Domain = Settings.OktaDomain,
        AuthorizationServerId = Settings.AuthorizationServerId,
        ClientId = Settings.ClientId,
        ClientSecret = Settings.ClientSecret,
        Scope = Settings.Scope,
        RedirectUri = Settings.RedirectUri,
        PostLogoutRedirectUri = Settings.PostLogoutRedirectUri
    });
}

CeCookieAuthenticationProvider is mostly the default CookieAuthenticationProvider. It overrides ResponseSignIn() so that I can set IsPersistent and AllowRefresh= true. It also sets and unsets a couple of application cookies on sign in and sign out.

Details of UseCeAuthMvc():

public static IAppBuilder UseCeAuthMvc(this IAppBuilder app, CeAuthOptions options)
{
    if (app == null)
    {
        throw new ArgumentNullException(nameof(app));
    }

    new CeAuthOptionsValidator().Validate(options);

    // Stop the default behavior of remapping JWT claim names to legacy MS/SOAP claim names
    JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

    var openIdConnectOptions = new OpenIdConnectAuthenticationOptionsBuilder(options).BuildOpenIdConnectAuthenticationOptions();
    app.UseRefreshToken(new RefreshTokenMiddlewareOptions(openIdConnectOptions));
    app.UseOpenIdConnectAuthentication(openIdConnectOptions);
    return app;
}

Details of BuildOpenIdConnectAuthenticationOptions():

internal OpenIdConnectAuthenticationOptions BuildOpenIdConnectAuthenticationOptions()
{
    var issuerUrl = CeAuthUrlHelper.CreateIssuerUrl(_options.Domain, _options.AuthorizationServerId);
    var notificationHandler = new OidcNotificationHandler();
    var securityTokenNotificationHandler = new SecurityTokenNotificationHandler(issuerUrl);
    var openIdConnectOptions = new OpenIdConnectAuthenticationOptions
    {
        ClientId = _options.ClientId,
        ClientSecret = _options.ClientSecret,
        Authority = issuerUrl,
        RedirectUri = _options.RedirectUri,
        ResponseType = OpenIdConnectResponseType.Code,
        RedeemCode = true,
        SaveTokens = true,
        UseTokenLifetime = false, // Must be false in order to use CookieAuthenticationOptions.ExpireTimeSpan
        Scope = string.Join(" ", (_options.Scope?.ToArray()) ?? CeAuthDefaults.Scope),
        PostLogoutRedirectUri = _options.PostLogoutRedirectUri,
        TokenValidationParameters = new TokenValidationParameters
        {
            NameClaimType = "name",
            ValidAudience = _options.ClientId,
            RequireExpirationTime = true,
            RequireSignedTokens = true,
            ValidateIssuer = true,
            ValidIssuer = issuerUrl,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(2),
        },
        SecurityTokenValidator = new StrictSecurityTokenValidator(),
        AuthenticationMode = AuthenticationMode.Passive,
        Notifications = new OpenIdConnectAuthenticationNotifications()
        {
            AuthenticationFailed = notificationHandler.OnAuthenticationFailed,
            AuthorizationCodeReceived = notificationHandler.OnAuthorizationCodeReceived,
            MessageReceived = notificationHandler.OnMessageReceived,
            RedirectToIdentityProvider = notificationHandler.BeforeRedirectToIdentityProviderAsync,
            SecurityTokenReceived = securityTokenNotificationHandler.OnSecurityTokenReceived,
            SecurityTokenValidated = securityTokenNotificationHandler.OnSecurityTokenValidatedAsync,
            TokenResponseReceived = notificationHandler.OnTokenResponseReceived,
        }
    };
    return openIdConnectOptions;
}

OnSecurityTokenValidatedAsync

public async Task OnSecurityTokenValidatedAsync(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
    //context.AuthenticationTicket.Properties.AllowRefresh = true;
    //context.AuthenticationTicket.Properties.IsPersistent = true;

    context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypeKey.IdToken, context.ProtocolMessage.IdToken));
    context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypeKey.AccessToken, context.ProtocolMessage.AccessToken));

    if (!string.IsNullOrEmpty(context.ProtocolMessage.RefreshToken))
        context.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypeKey.RefreshToken, context.ProtocolMessage.RefreshToken));

    FillNameIdentifierClaimOnIdentity(context.AuthenticationTicket.Identity);

    if (_getClaimsFromUserInfoEndpoint)
    {
        await _userInformationProvider.EnrichIdentityViaUserInfoAsync(context.AuthenticationTicket.Identity, context.ProtocolMessage.AccessToken).ConfigureAwait(false);
    }
    return;
}

private void FillNameIdentifierClaimOnIdentity(ClaimsIdentity identity)
{
    var currentNameIdentifier = identity.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value;
    var sub = identity.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;

    if (currentNameIdentifier == null && sub != null)
    {
        identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, sub));
    }
}
laura-rodriguez commented 4 years ago

Thanks Mark!

I want to keep exploring the Challenge scenario a bit more to understand why it didn't work, so let me ask you some follow-up questions.

You mentioned that when you tried Challenge before, the problem you had was that the users were redirected to the Okta login page instead of yours. This usually happens when your OIDC AuthorizationMode is not set to AuthenticationMode.Passive. Did you have this set when trying that? You also need to configure the Cookie middleware with your application login path. There's also a prompt parameter you can pass that might help with Okta sign-in redirect issue. Have you also explored this?

Sorry if I keep asking more questions, I just want to make sure what scenarios you tried and why they didn't work.

Good-man commented 4 years ago

Hi Laura,

I really appreciate your questions! Your questions will help us figure this out together. I love it! šŸ˜

I have always had AuthenticationMode = AuthenticationMode.Passive. My cookie authentication middleware has always been set to LoginPath = new PathString("/Account/Login"), which is what I want.

Thought: I suspect that the refresh_token challenge without the Okta session cookie was rejected by Okta because it didn't contain everything that was required. During the normal flow, the challenge only contains a sesstionToken and a RedirectUri. But during a request when the "access token" is almost (or actually) expired, all I have is a refresh, id, and access token (which may or may not be expired at this point). What would a valid challenge with these things look like? šŸ¤·ā€ā™‚ļø

I agree... A "challenge" with a refresh token makes sense. I hope it's that simple. I hope that I am just missing something required with something I have.

Mark

laura-rodriguez commented 4 years ago

Hi @Good-man ,

Yes, you are right, when you are using a custom login page you need to pass the session id as a parameter.

As far as I can see, the issue here is not with the SDK because you are able to retrieve the tokens, but with Owin and what is the recommended way to update the cookie once you have the updated tokens.

I think this ASP.NET Core sample describes what you are trying to do. I was looking into the equivalent way in ASP.NET, and I think what you can try is moving your token logic into the ValidateIdentity method in your CeCookieAuthenticationProvider.

I did a quick test trying to overwrite the tokens, and it seems to work:

            /// <summary>
            /// Implements the interface method by invoking the related delegate method
            /// </summary>
            /// <param name="context"></param>
            /// <returns></returns>
            public virtual Task ValidateIdentity(CookieValidateIdentityContext context)
            {

                var identity = (ClaimsIdentity)context.Identity;
                var accessTokenClaim = identity.FindFirst("access_token");
                var refreshTokenClaim = identity.FindFirst("refresh_token");
                // everything went right, remove old tokens and add new ones
                identity.RemoveClaim(accessTokenClaim);
                identity.RemoveClaim(refreshTokenClaim);
                identity.AddClaims(new[]
                                  {
                                        new Claim("access_token", "foo"),
                                        new Claim("refresh_token", "bar")
                                    });

                context.ReplaceIdentity(identity);
                return Task.FromResult<object>(null);
            }

Let me know if this helps.

Good-man commented 4 years ago

Hi @laura-rodriguez ,

This makes sense, but it does not cause the cookie middleware to set a new cookie. So, on the next request the browser passes the old cookie, claims, and access token which is still expired. :-(

Mark

Good-man commented 4 years ago

@laura-rodriguez the ResponseSignIn method allows changing the claims before they are converted into a cookie, but it only happens during SignIn. Not on each request.

/// <summary>
/// Called when an endpoint has provided sign in information before it is converted into a cookie. 
/// By implementing this method, the claims and extra information that go into the ticket may be altered.
/// </summary>
/// <param name="context"></param>
public override void ResponseSignIn(CookieResponseSignInContext context)
Good-man commented 4 years ago

Hi @laura-rodriguez ,

I think that I may have just gotten it to work. I removed the code from the ValidateIdentity method. I modified the code in my RefreshTokenMiddleware to remove and add the new claims (instead of creating a new ClaimsIdentitylike I was doing before), then called the .SignIn() method again... which triggered the .ResponseSignIn() method to execute in the cookie middleware. At this point, the middleware has the new tokens before the information is converted into a cookie!!

I'm still testing but this is very encouraging!

Mark

laura-rodriguez commented 4 years ago

Hi @Good-man,

I'm crossing fingers šŸ¤ž ! Please, let me know how it goes.

Good-man commented 4 years ago

Hi @laura-rodriguez, (cc @chrismorris-okta)

So far, in all my testing, it is working perfectly! šŸ‘

I believe that this could be added to the Okta ASP.net SDK fairly easily and it would be amazing! Is there anything that I can do to help with this? A pull request?

Mark

laura-rodriguez commented 4 years ago

Hi @Good-man,

That's awesome! šŸŽ‰

We definitely want to know what features you think could be added to the SDK to make the experience better. So, if you have a minimal working project that you can share with us, we can definitely take a look at the bits that can be added to both ASP.NET and ASP.NET Core SDKs. If you already have an idea, feel free to share your thoughts and/or submit a PR ā¤ļø .

We try to make both SDKs consistent in terms of features that they support, so anything we decide to add to ASP.NET we will also spend some time adding it to ASP.NET Core.

naveenkv66 commented 4 years ago

@Good-man can you share the refresh token implementation full working code.

Good-man commented 3 years ago

@naveenkv66

public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                //CookieName = ".AspNet.Cookies",  // this is the default
                LoginPath = new PathString("/Account/Login"),
                // This is critical!  See https://github.com/aspnet/AspNetKatana/wiki/System.Web-response-cookie-integration-issues
                CookieManager = new SystemWebCookieManager(),
                CookieSameSite = SameSiteMode.Lax, // Cannot be set to Strict!
                // Note: OpenIdConnectAuthenticationOptions.UseTokenLifetime must be false for ExpireTimeSpan and SlidingExpiration to be honored
                // ExpireTimeSpan = TimeSpan,  // the cookie won't persist unless IsPersistent is set during SignIn
                SlidingExpiration = true,
                Provider = new CookieAuthenticationProvider()
                {
                    OnResponseSignIn = (context) =>
                    {
                        // placeholder for additional sign in code

                    },
                    OnResponseSignOut = (context) =>
                    {
                        SensitiveResourceCookie.Expire(context.Response.Cookies);
                        RememberMeCookie.Expire(context.Response.Cookies);
                    }
                }
            });

            var scope = OktaSettings.Scope ?? CeAuthDefaults.Scope;
            app.UseRefreshToken(new RefreshTokenMiddlewareOptions()
            {
                Domain = OktaSettings.Domain,
                TokenPath = "/v1/token",
                IntrospectPath = "/v1/introspect",
                AuthorizationServerId = OktaSettings.AuthorizationServerId,
                ClientId = OktaSettings.ClientId,
                ClientSecret = OktaSettings.ClientSecret,
                Scope = scope
            });
            app.UseCeAuthMvc(new CeAuthMvcOptions
            {
                Domain = OktaSettings.Domain,
                AuthorizationServerId = OktaSettings.AuthorizationServerId,
                ClientId = OktaSettings.ClientId,
                ClientSecret = OktaSettings.ClientSecret,
                IntrospectPath = "/v1/introspect",
                Scope = scope,
                RedirectUri = OktaSettings.RedirectUri,
                UsePkce = true,
                PostLogoutRedirectUri = OktaSettings.PostLogoutRedirectUri,
                UserInformationProvider = new OktaUserInformationProvider(OktaSettings.Domain, OktaSettings.AuthorizationServerId),
                AuthenticationFailed = (context) =>
                {
                    // this is an example of how to handle authentication failed events
                    var url = new UrlHelper(System.Web.HttpContext.Current.Request.RequestContext)
                        .Action("AuthenticationFailed", "Error", new { message = context.ProtocolMessage.ErrorDescription });
                    context.Response.Redirect(url);
                    context.HandleResponse();
                    return Task.FromResult(0);
                },
                RedirectToIdentityProvider = n =>
                {
                    // placeholder for idp redirect hooks
                    return Task.CompletedTask;
                },
                AuthorizationCodeReceived = n =>
                {
                    // placeholder for authorization code received hooks
                    return Task.CompletedTask;
                }
            });
        }
    }
    public class RefreshTokenMiddleware : OwinMiddleware
    {
        private readonly RefreshTokenMiddlewareOptions _options;

        public RefreshTokenMiddleware(OwinMiddleware next, RefreshTokenMiddlewareOptions options) : base(next)
        {
            _options = options;
        }

        public override async Task Invoke(IOwinContext context)
        {
            if (AccessTokenExpired(context) && HasRefreshToken(context))
            {
                // TODO: find a way to avoid refreshing tokens when signing out

                await RefreshTokens(context);
            }
            await Next.Invoke(context);
        }

        private bool HasRefreshToken(IOwinContext context)
        {
            if (context.Authentication.User.Identity is ClaimsIdentity id && id.IsAuthenticated)
                return id.Claims.Any(c => c.Type == ClaimTypeKey.RefreshToken);
            return false;
        }

        private bool AccessTokenExpired(IOwinContext context)
        {
            if (context.Authentication.User.Identity is ClaimsIdentity id && id.IsAuthenticated)
            {
                var accessTokenString = id.FindFirst(ClaimTypeKey.AccessToken)?.Value;
                if (accessTokenString == null)
                    return false;
                var accessToken = new StrictTokenHandler().ReadToken(accessTokenString);

                if (accessToken.ValidTo <= DateTime.UtcNow)
                    return true;

                return false;
            }
            return false;
        }

        private async Task RefreshTokens(IOwinContext context)
        {
            var identity = (ClaimsIdentity)context.Authentication.User.Identity;
            var refreshTokenClaim = identity.FindFirst(ClaimTypeKey.RefreshToken);
            if (refreshTokenClaim != null)
            {
                var accessTokenClaim = identity.FindFirst(ClaimTypeKey.AccessToken);
                var idTokenClaim = identity.FindFirst(ClaimTypeKey.IdToken);

                identity.RemoveClaim(refreshTokenClaim);
                identity.TryRemoveClaim(accessTokenClaim);
                identity.TryRemoveClaim(idTokenClaim);

                var client = new TokenHandler(_options.ClientId, _options.ClientSecret)
                {
                    IntrospectAddress = CeAuthUrlHelper.CreateTokenUrl(_options.Domain, _options.AuthorizationServerId, _options.TokenPath),
                };
                var tokenResponse = await client.RequestRefreshToken(refreshTokenClaim.Value);
                if (tokenResponse.IsError) throw new TokenException(tokenResponse.Error);

                identity.AddClaims(new[] {
                        new Claim(ClaimTypeKey.AccessToken, tokenResponse.AccessToken),
                        new Claim(ClaimTypeKey.IdToken, tokenResponse.IdentityToken),
                        new Claim(ClaimTypeKey.RefreshToken, tokenResponse.RefreshToken),
                    });

                var persist = RememberMeCookie.Exists(context.Request.Cookies);
                var address = CeAuthUrlHelper.CreateIntrospectUrl(_options.Domain, _options.AuthorizationServerId, _options.IntrospectPath);
                var clientId = _options.ClientId;
                var clientSecret = _options.ClientSecret;
                var introspectUtil = new TokenHandler(clientId, clientSecret) { IntrospectAddress = address };
                var expiresUtc = TokenHandler.GetExpiration(await introspectUtil.Introspect(tokenResponse.RefreshToken, "refresh_token"));

                context.Authentication.SignIn(new AuthenticationProperties
                {
                    ExpiresUtc = expiresUtc,
                    IsPersistent = persist,
                    AllowRefresh = persist,
                }, identity);
            }
        }
    }
mrtariqmahmood commented 3 years ago

I am trying the code in the post but always get tokenResponse.IsError to be true.

var tokenClient = new TokenClient(authority + "/v1/token", clientId, clientSecret); var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, authority); if (tokenResponse.IsError) { throw new Exception(tokenResponse.Error); }

The error is

{"error":"invalid_grant","error_description":"The 'redirect_uri' does not match the redirection URI used in the authorization request."}

I have thoroughly checked redirect_uri is correctly and is registered.

I don't understand why any ideas?

Good-man commented 3 years ago

Make sure that the grant type you're using is enabled in Okta.

On Mon, Apr 26, 2021, 6:23 AM mrtariqmahmood @.***> wrote:

I am trying the code in the post but always get tokenResponse.IsError to be true.

var tokenClient = new TokenClient(authority + "/v1/token", clientId, clientSecret); var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, authority); if (tokenResponse.IsError) { throw new Exception(tokenResponse.Error); }

The error is

{"error":"invalid_grant","error_description":"The 'redirect_uri' does not match the redirection URI used in the authorization request."}

I have thoroughly checked redirect_uri is correctly and is registered.

I don't understand why any ideas?

ā€” You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/okta/okta-aspnet/issues/130#issuecomment-826714490, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABCEG3OZLLQJZR25LTDTU63TKU5LPANCNFSM4OWRPDMQ .