okta / okta-aspnet

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

Blazor .NET 8 Error #260

Closed jmiller76 closed 6 months ago

jmiller76 commented 7 months ago

Describe the bug?

After updating to 4.6, my Blazor app is failing to authenticate. Can reproduce with .NET 6 Sample code (after upgrading to .NET 8)

What is expected to happen?

Authentication

What is the actual behavior?

An unhandled exception occurred while processing the request. SecurityTokenException: The Validated Security Token must be of type JsonWebToken, but instead its type is 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'. Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.ValidateTokenUsingHandlerAsync(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters)

AuthenticationFailureException: An error was encountered while handling the remote login. Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()

Reproduction Steps?

Pull https://github.com/okta/samples-blazor, use Upgrade Assistant to move the demo to .NET 8. Update Appsettings to working API keys Test - All works Update NuGet package Okta.AspNetCore to 4.6.0 Test - Above Error show

Additional Information?

No response

.NET Version

8.0.200

SDK Version

4.6

OS version

BuildNumber Caption OSArchitecture Version 22631 Microsoft Windows 11 Enterprise 64-bit 10.0.22631

laura-rodriguez commented 7 months ago

Thanks for reporting this issue @jmiller76!

Possible duplicated of #259

jmiller76 commented 7 months ago

Possibly, I am monitoring that one too.

The error seemed different enough that I thought it may be a different use case.

This also produces with a less complicated AddOktaMvc. If there is a common problem that may help.

I think this may help too.

yongkeecho commented 7 months ago

The issues were gone when all the venerable versions are updated to the latest ones. But I still had an issue with 4.6.0 in the previous post.

THJeffJ commented 7 months ago

It looks like this is related to a .NET 8 behavior breaking change - Security token events return a JsonWebToken

GeoRHeld commented 7 months ago

Having this same issue when upgrading to 4.6.0 on an ASP.NET 8.0 Razor Pages project. Downgrading to 4.5.0 works with the "fix" described in issue #259 .

THJeffJ commented 7 months ago

@laura-rodriguez Reverting to using SecurityTokenValidators in OpenIdConnectOptions and JwtBearerOptions within OpenIdConnectOptionsHelper as described in the MS article above will resolve this. I tested this change briefly. #259 appears to be a different issue.

Mark-Good commented 7 months ago

This issue is not limited to Blazor. It is also impacting my ASP.NET Core MVC application in the same way as described.

davidcorbin-atmosera commented 6 months ago

Note: The Okta provided sample: https://github.com/okta/samples-blazor

Is a reproducible link once upgraded!!!

lymberl commented 6 months ago

@THJeffJ Were you able to fix this issue by reverting to using SecurityTokenValidators? Are you able to post the code for this?

davidcorbin-atmosera commented 6 months ago

Note: The Okta provided sample: https://github.com/okta/samples-blazor

Is a reproducible link once upgraded!!!

@lymberl - Please note, that: 1) I was NOT able to find a way to set the parameter to use the old method (I spotted the conditional in the Okta source), but if I did modify the Okta source to set it [unacceptable] the program 'worked". 2) Using the old Token types leaves open a known security hole [as does reverting back to Okta 4.5] that has been documented: https://github.com/advisories/GHSA-59j7-ghrg-fj52 int he current referenced Microsoft asemble. This is also unacceptable from a security standpoint...

As far as I can tell [and I am not claiming expertise] there needs to be a way to configure the Okta side of things [aka the application] that it shoudl return the new type of token rather than the old.....and then ALSO update Okta so it does not pull in old [2022] assemblies with known security alerts...

NickDrouin commented 6 months ago

Same issue, here's my notes:

Microsoft.AspNetCore.Authentication.AuthenticationFailureException: An error was encountered while handling the remote login.
 ---> Microsoft.IdentityModel.Tokens.SecurityTokenException: 
 The Validated Security Token must be of type JsonWebToken, but instead its type is 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken'.
   at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.ValidateTokenUsingHandlerAsync(String idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters)
   at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync()
   --- End of inner exception stack trace ---
   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

Seems related to this: https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/8.0/securitytoken-events

Which seems known by Okta: https://github.com/okta/okta-aspnet/issues/260

The failure is at: OpenIdConnectHandler.cs#L532

                if (!base.Options.UseSecurityTokenValidator)
                {
                    TokenValidationResult obj = await ValidateTokenUsingHandlerAsync(tokenEndpointResponse2.IdToken, properties, validationParameters);
                    user4 = new ClaimsPrincipal(obj.ClaimsIdentity);
                    jwt3 = JwtSecurityTokenConverter.Convert(obj.SecurityToken as JsonWebToken);
                }
                else
                {
                    user4 = ValidateToken(tokenEndpointResponse2.IdToken, properties, validationParameters, out jwt3);
                }

Where: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectOptions.UseSecurityTokenValidator==false

_Note: If UseSecurityTokenValidator is set to true in debug, we login without issues.

The error is in the delegate to Okta within:

    private async Task<TokenValidationResult> ValidateTokenUsingHandlerAsync(string idToken, AuthenticationProperties properties, TokenValidationParameters validationParameters)
    {
        if (base.Options.ConfigurationManager is BaseConfigurationManager configurationManager)
        {
            validationParameters.ConfigurationManager = configurationManager;
        }
        else if (_configuration != null)
        {
            string[] array = new string[1] { _configuration.Issuer };
            validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(array) ?? array;
            validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys;
        }
        TokenValidationResult tokenValidationResult = await base.Options.TokenHandler.ValidateTokenAsync(idToken, validationParameters);

Where base.Options.TokenHandler is of type: {Okta.AspNet.Abstractions.StrictTokenHandler} With:

The code continues with:

        if (tokenValidationResult.Exception != null)
        {
            throw tokenValidationResult.Exception;
        }
        SecurityToken securityToken = tokenValidationResult.SecurityToken;
        if (!tokenValidationResult.IsValid || securityToken == null)
        {
            base.Logger.UnableToValidateIdTokenFromHandler(idToken);
            throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.UnableToValidateTokenFromHandler, idToken));
        }
        if (!(securityToken is JsonWebToken))
        {
            base.Logger.InvalidSecurityTokenTypeFromHandler(securityToken?.GetType());
            throw new SecurityTokenException(string.Format(CultureInfo.InvariantCulture, Resources.ValidatedSecurityTokenNotJsonWebToken, securityToken?.GetType()));
        }

Where securityToken is of type Microsoft.IdentityModel.Tokens.SecurityToken {System.IdentityModel.Tokens.Jwt.JwtSecurityToken}

So the following is logged:

Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler: Error: The Validated Security Token must be of type JsonWebToken, but instead its type is: 'System.IdentityModel.Tokens.Jwt.JwtSecurityToken.'

And the code throws.

In my case, I created a new net8.0 Blazor project, server-side only, and manually applied all the changes using the github sample (the sample github project is way out of date compared to the latest dotnet new for such a project).

NickDrouin commented 6 months ago

Following on, here is my work-around:

In my Program.cs:

             // Add Okta Auth:
            builder.Services.AddAuthentication(options =>
                {
                    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
                })
                .AddCookie()
                .AddOktaMvc(new OktaMvcOptions
                {
                    // Replace the Okta placeholders in appsettings.json with your Okta configuration.
                    OktaDomain = builder.Configuration.GetValue<string>("Okta:OktaDomain"),
                    ClientId = builder.Configuration.GetValue<string>("Okta:ClientId"),
                    ClientSecret = builder.Configuration.GetValue<string>("Okta:ClientSecret"),
                    AuthorizationServerId = builder.Configuration.GetValue<string>("Okta:AuthorizationServerId"),
                    OpenIdConnectEvents = new OpenIdConnectEvents()
                     {                        
                        OnTokenResponseReceived = async context =>
                        {
                            // Make our own call to Okta.AspNet.Abstractions.StrictTokenHandler
                            // First setup the parameterization as per OpenIdConnectHandler.cs:
                            var _configuration = context.Options.Configuration;
                            TokenValidationParameters validationParameters = context.Options.TokenValidationParameters.Clone();
                            validationParameters.RequireSignedTokens = false;
                            if (context.Options.ConfigurationManager is BaseConfigurationManager configurationManager)
                            {
                                validationParameters.ConfigurationManager = configurationManager;
                            }
                            else if (_configuration != null)
                            {
                                string[] array = new string[1] { _configuration.Issuer };
                                validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(array) ?? array;
                                validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys;
                            }
                            string idToken = context.TokenEndpointResponse.IdToken;

                            // Call the validation and check
                            TokenValidationResult tokenValidationResult = await context.Options.TokenHandler.ValidateTokenAsync(idToken, validationParameters);
                            if (tokenValidationResult.Exception != null)
                            {
                                throw tokenValidationResult.Exception;
                            }

                            // Since we have already done the validation, avoid doing again with the net8.0 code
                            // which throws because of Okta's use of JwtSecurityToken instead of JsonWebToken:
                            context.Options.UseSecurityTokenValidator = true;
                        }
                     },
                });
            // To pick up AccountController:
            builder.Services.AddControllers();

Obviously, it doesn't solve the core issue within the Okta library, but I think it does a reasonable job at validating both within the Okta lib, and the secondary validation using the bypass path in net8.0 for JwtSecurityToken.

This code isn't in production, so my security bar is much lower than some readers. Use at your own risks and perils.

davidcorbin-atmosera commented 6 months ago

> Following on, here is my work-around: @NickDrouin - Your analysis basically matches mine... However, I am not sure I see that as a work around [it is effectively reverting back to the Okta 4.5 behavior, using the old type of tokens, which I believe (and upon a re-read also see you mention) brings back the security issue of: (Note this gets flagged as a security issue until Okta is updated to NOT reference that Microsoft DLL. But at least in the broken case, the insecure code is not actively executed. It would seem that the "solution" is to update the Okta Authentication serverso that it returns the newer (and more secure/performant) token returned when one authenticates against: https://dev-4xxxxxxxx.okta.com/.....

NickDrouin commented 6 months ago

@davidcorbin-atmosera : It would seem that the "solution" is to update the Okta Authentication server so that it returns the newer (and more secure/performant) token returned when one authenticates against: https://dev-4xxxxxxxx.okta.com/.....

Right, that's out of my hands, as a user.

lymberl commented 6 months ago

I agree. Okta needs to fix this so that we can use their latest Nuget package Okta.AspNetCore with .NET8. Lets hope they fix it quickly otherwise I cant use Okta login until they do.

NickDrouin commented 6 months ago

I agree. Okta needs to fix this so that we can use their latest Nuget package Okta.AspNetCore with .NET8. Lets hope they fix it quickly otherwise I cant use Okta login until they do.

To be fair, with the work-around above (and I'm open to code-review feedback, auth isn't my area of expertise), or any other: we are just dropping back to what there was before. It is MSFT that has raised the bar in .net8.0 and Okta hasn't caught up. So, this isn't as much a regression as a breaking change; with a work-around.

lymberl commented 6 months ago

I have tried this and other similar workarounds and it gets rid of the error message but after the OnTokenResponseReceived method finishes it just keeps redirecting back to my login method. Inside my login method HttpContext.User.Identity.IsAuthenticated is still false. And since this is false the challenge is executed and goes back to the OnTokenResponseReceived with new tokens in an endless loop. Since its false the claims information is also never populated and I cannot get any user information either.

This is my login method:

public async Task<ActionResult> LoginOkta()
{
    if (!HttpContext.User.Identity.IsAuthenticated) //this is always false even after OnTokenResponseReceived  is executed
    {
        return Challenge(OktaDefaults.MvcAuthenticationScheme);
    }

//do rest of login logic

}

I am also using the ASPNET login by using the AddDefaultIdentity in additiona to the Okta login in the program.cs file:

builder.Services.AddDefaultIdentity<WebUser>(options => options.SignIn.RequireConfirmedAccount = false)
    .AddRoles<IdentityRole>()
    .AddEntityFrameworkStores<SecurityContext>();

builder.Services.ConfigureApplicationCookie(options =>
{
    options.LoginPath = "/account/login";
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always;
}).AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOktaMvc(new OktaMvcOptions
{
    // Replace these values with your Okta configuration
    OktaDomain = builder.Configuration.GetValue<string>("Okta:OktaDomain"),
    AuthorizationServerId = builder.Configuration.GetValue<string>("Okta:AuthorizationServerId"),
    ClientId = builder.Configuration.GetValue<string>("Okta:ClientId"),
    ClientSecret = builder.Configuration.GetValue<string>("Okta:ClientSecret"),
    Scope = new List<string> { "openid", "profile", "email" },
    OpenIdConnectEvents = new OpenIdConnectEvents()
    {
        OnTokenResponseReceived = async context =>
        {
            // Make our own call to Okta.AspNet.Abstractions.StrictTokenHandler
            // First setup the parameterization as per OpenIdConnectHandler.cs:
            var _configuration = context.Options.Configuration;
            TokenValidationParameters validationParameters = context.Options.TokenValidationParameters.Clone();
            validationParameters.RequireSignedTokens = false;
            if (context.Options.ConfigurationManager is BaseConfigurationManager configurationManager)
            {
                validationParameters.ConfigurationManager = configurationManager;
            }
            else if (_configuration != null)
            {
                string[] array = new string[1] { _configuration.Issuer };
                validationParameters.ValidIssuers = validationParameters.ValidIssuers?.Concat(array) ?? array;
                validationParameters.IssuerSigningKeys = validationParameters.IssuerSigningKeys?.Concat(_configuration.SigningKeys) ?? _configuration.SigningKeys;
            }
            string idToken = context.TokenEndpointResponse.IdToken;

            // Call the validation and check
            TokenValidationResult tokenValidationResult = await context.Options.TokenHandler.ValidateTokenAsync(idToken, validationParameters);
            if (tokenValidationResult.Exception != null)
            {
                throw tokenValidationResult.Exception;
            }

            // Since we have already done the validation, avoid doing again with the net8.0 code
            // which throws because of Okta's use of JwtSecurityToken instead of JsonWebToken:
            context.Options.UseSecurityTokenValidator = true;
        }
    },
});

In the app admin dashboard the Initiate login URI is set to the login method above. Untitled-1

laura-rodriguez commented 6 months ago

Hi everyone!

Thank you so much for your patience and contributions in this thread 💟

I've just submitted a draft PR (I still need to perform additional testing) with the potential fix for this issue. Feel free to chime in and provide feedback!

If anyone wants to test it and needs a temp NuGet package, let me know!

davidcorbin-atmosera commented 6 months ago

Laura, great to hear that there is a potential fix. It's late here. I am East Coast but if you let me know where the Nuget packages or email it to me, I will be able to check it tomorrow.

Get Outlook for iOShttps://aka.ms/o0ukef


From: Laura Rodríguez @.> Sent: Monday, April 15, 2024 5:30:23 PM To: okta/okta-aspnet @.> Cc: David Corbin @.>; Mention @.> Subject: [External] Re: [okta/okta-aspnet] Blazor .NET 8 Error (Issue #260)

Hi everyone!

Thank you so much for your patience and contributions in this thread 💟

I've just submitted a draft PR https://github.com/okta/okta-aspnet/pull/262 (I still need to perform additional testing) with the potential fix for this issue. Feel free to chime in and provide feedback!

If anyone wants to test it and needs a temp NuGet package, let me know!

— Reply to this email directly, view it on GitHubhttps://github.com/okta/okta-aspnet/issues/260#issuecomment-2057843011, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AT3Q5VDWFS2IZFYDME7ANG3Y5RBG7AVCNFSM6AAAAABEQ23QKKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANJXHA2DGMBRGE. You are receiving this because you were mentioned.Message ID: @.***>

This message was sent from outside the company. Please do not click links or open attachments unless you recognize the source of this email and know the content is safe.

NickDrouin commented 6 months ago

See my comments in the PR.

NickDrouin commented 6 months ago

I have made a pull request to update the Blazor server-side sample here: https://github.com/okta/samples-blazor/pull/20

It currently has my work-around -- it may be of interest to others.

laura-rodriguez commented 6 months ago

Laura, great to hear that there is a potential fix. It's late here. I am East Coast but if you let me know where the Nuget packages or email it to me, I will be able to check it tomorrow. Get Outlook for iOShttps://aka.ms/o0ukef ____ From: Laura Rodríguez @.> Sent: Monday, April 15, 2024 5:30:23 PM To: okta/okta-aspnet @.> Cc: David Corbin @.>; Mention @.> Subject: [External] Re: [okta/okta-aspnet] Blazor .NET 8 Error (Issue #260) Hi everyone! Thank you so much for your patience and contributions in this thread 💟 I've just submitted a draft PR <#262> (I still need to perform additional testing) with the potential fix for this issue. Feel free to chime in and provide feedback! If anyone wants to test it and needs a temp NuGet package, let me know! — Reply to this email directly, view it on GitHub<#260 (comment)>, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AT3Q5VDWFS2IZFYDME7ANG3Y5RBG7AVCNFSM6AAAAABEQ23QKKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANJXHA2DGMBRGE. You are receiving this because you were mentioned.Message ID: @.***> This message was sent from outside the company. Please do not click links or open attachments unless you recognize the source of this email and know the content is safe.

Hey @davidcorbin-atmosera ,

I've attached the testing artifacts in this comment: https://github.com/okta/okta-aspnet/pull/262#issuecomment-2059715435

Thank you!

NickDrouin commented 6 months ago

@laura-rodriguez : what is the issue / tracking number of the feature that will restore the Okta StrictTokenHandler after it was removed with PR https://github.com/okta/okta-aspnet/pull/262 ?

Or, has Okta taken the design decision to deprecate it in favor of the base security token validator?

What is the roadmap to moving towards the JsonWebToken ?

[Copied from PR thread.]

laura-rodriguez commented 6 months ago

@NickDrouin It's an internal JIRA ticket, but I've just filed a GH issue with a link to the internal ticket, so it's visible to everyone.

Thank you all!