Home > database >  IdentityServer4 'invalid_grant" error after OpenIdConnectEvents OnAuthorizationCodeReceive
IdentityServer4 'invalid_grant" error after OpenIdConnectEvents OnAuthorizationCodeReceive

Time:09-30

In the process of converting from .NET 4.8 to .NET 6. Ran into an issue when attempting to authenticate sign-in with Identity Server 4.

I get to the log-in page, I log-in to the auth, but don't get redirected correctly it seems.

Everything seems to work fine until AFTER the OnAuthorizationCodeReceived is finished while configuring SetOpenIdConnectOptions.

The Exception: OpenIdConnectProtocolException: Message contains error: 'invalid_grant', error_description: 'error_description is null', error_uri: 'error_uri is null'. Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RedeemAuthorizationCodeAsync(OpenIdConnectMessage tokenEndpointRequest)

Here is the LOG:

2022-09-26 14:33:56.602 -08:00 [DBG] local found in database: true
2022-09-26 14:33:56.602 -08:00 [DBG] client configuration validation for client local succeeded.
2022-09-26 14:33:56.602 -08:00 [DBG] Secret validator success: HashedSharedSecretValidator
2022-09-26 14:33:56.602 -08:00 [DBG] Client validation success
2022-09-26 14:33:56.602 -08:00 [INF] {"ClientId":"local","AuthenticationMethod":"SharedSecret","Category":"Authentication","Name":"Client Authentication Success","EventType":"Success","Id":1010,"Message":null,"ActivityId":"8000022c-0000-fc00-b63f-84710c7967bb","TimeStamp":"2022-09-26T22:33:56.0000000Z","ProcessId":16860,"LocalIpAddress":"::1:44318","RemoteIpAddress":"::1","$type":"ClientAuthenticationSuccessEvent"}
2022-09-26 14:33:56.602 -08:00 [DBG] Start token request validation
2022-09-26 14:33:56.603 -08:00 [DBG] Start client credentials token request validation
2022-09-26 14:33:56.604 -08:00 [DBG] Found [] identity scopes in database
2022-09-26 14:33:56.606 -08:00 [DBG] Found ["Api"] API resources in database
2022-09-26 14:33:56.608 -08:00 [DBG] Found ["MY.API"] scopes in database
2022-09-26 14:33:56.608 -08:00 [DBG] local credentials token request validation success
2022-09-26 14:33:56.608 -08:00 [INF] Token request validation success, {"ClientId":"local","ClientName":"(Local)","GrantType":"client_credentials","Scopes":"MY.API","AuthorizationCode":"*******","RefreshToken":"*******","UserName":null,"AuthenticationContextReferenceClasses":null,"Tenant":null,"IdP":null,"Raw":{"grant_type":"client_credentials","scope":"MY.API","client_id":"local","client_secret":"***REDACTED***"},"$type":"TokenRequestValidationLog"}
2022-09-26 14:33:56.608 -08:00 [DBG] Getting claims for access token for client: local
2022-09-26 14:33:56.609 -08:00 [INF] {"ClientId":"local","ClientName":"Local","RedirectUri":null,"Endpoint":"Token","SubjectId":null,"Scopes":"MY.API","GrantType":"client_credentials","Tokens":[{"TokenType":"access_token","TokenValue":"****KMHw","$type":"Token"}],"Category":"Token","Name":"Token Issued Success","EventType":"Success","Id":2000,"Message":null,"ActivityId":"8000022c-0000-fc00-b63f-84710c7967bb","TimeStamp":"2022-09-26T22:33:56.0000000Z","ProcessId":16860,"LocalIpAddress":"::1:44318","RemoteIpAddress":"::1","$type":"TokenIssuedSuccessEvent"}
2022-09-26 14:33:56.609 -08:00 [DBG] Token request success.
2022-09-26 14:33:57.683 -08:00 [DBG] Request path /connect/token matched to endpoint type Token
2022-09-26 14:33:57.683 -08:00 [DBG] Endpoint enabled: Token, successfully created handler: IdentityServer4.Endpoints.TokenEndpoint
2022-09-26 14:33:57.683 -08:00 [INF] Invoking IdentityServer endpoint: IdentityServer4.Endpoints.TokenEndpoint for /connect/token
2022-09-26 14:33:57.683 -08:00 [DBG] Start token request.
2022-09-26 14:33:57.683 -08:00 [DBG] Start client validation
2022-09-26 14:33:57.683 -08:00 [DBG] Start parsing Basic Authentication secret
2022-09-26 14:33:57.683 -08:00 [DBG] Start parsing for secret in post body
2022-09-26 14:33:57.683 -08:00 [DBG] Parser found secret: PostBodySecretParser
2022-09-26 14:33:57.683 -08:00 [DBG] Secret id found: local
2022-09-26 14:33:57.693 -08:00 [DBG] local found in database: true
2022-09-26 14:33:57.693 -08:00 [DBG] client configuration validation for client local succeeded.
2022-09-26 14:33:57.693 -08:00 [DBG] Secret validator success: HashedSharedSecretValidator
2022-09-26 14:33:57.693 -08:00 [DBG] Client validation success
2022-09-26 14:33:57.693 -08:00 [INF] {"ClientId":"local","AuthenticationMethod":"SharedSecret","Category":"Authentication","Name":"Client Authentication Success","EventType":"Success","Id":1010,"Message":null,"ActivityId":"80000989-0000-ee00-b63f-84710c7967bb","TimeStamp":"2022-09-26T22:33:57.0000000Z","ProcessId":16860,"LocalIpAddress":"::1:44318","RemoteIpAddress":"::1","$type":"ClientAuthenticationSuccessEvent"}
2022-09-26 14:33:57.693 -08:00 [DBG] Start token request validation
2022-09-26 14:33:57.693 -08:00 [DBG] Start validation of authorization code token request
2022-09-26 14:33:57.694 -08:00 [DBG] pfmbWJpdPbS0PunlzI8nAcGvays25N/W0z/Rlg5idOU= found in database: false
2022-09-26 14:33:57.695 -08:00 [DBG] authorization_code grant with value: 7EBFC1FBBB972962CACE378660D3E258C812B170DB496347289E19960C400FA9 not found in store.
2022-09-26 14:33:57.695 -08:00 [ERR] Invalid authorization code{"code":"7EBFC1FBBB972962CACE378660D3E258C812B170DB496347289E19960C400FA9"}, details: {"ClientId":"local","ClientName":"Local","GrantType":"authorization_code","Scopes":null,"AuthorizationCode":"****0FA9","RefreshToken":"********","UserName":null,"AuthenticationContextReferenceClasses":null,"Tenant":null,"IdP":null,"Raw":{"client_id":"local","client_secret":"***REDACTED***","code":"7EBFC1FBBB972962CACE378660D3E258C812B170DB496347289E19960C400FA9","grant_type":"authorization_code","redirect_uri":"https://localhost:49611/signin-oidc"},"$type":"TokenRequestValidationLog"}
2022-09-26 14:33:57.695 -08:00 [INF] {"ClientId":"local","ClientName":"Loca","RedirectUri":null,"Endpoint":"Token","SubjectId":null,"Scopes":null,"GrantType":"authorization_code","Error":"invalid_grant","ErrorDescription":null,"Category":"Token","Name":"Token Issued Failure","EventType":"Failure","Id":2001,"Message":null,"ActivityId":"80000989-0000-ee00-b63f-84710c7967bb","TimeStamp":"2022-09-26T22:33:57.0000000Z","ProcessId":16860,"LocalIpAddress":"::1:44318","RemoteIpAddress":"::1","$type":"TokenIssuedFailureEvent"}

My StartUp class:

namespace Test
{
    public class Startup
    {
        public IConfiguration Configuration { get; }
        public string MembershipRole1 { get; private set; }
        public string MembershipRole2 { get; private set; }
        public string MembershipRole3 { get; private set; }
        public string MembershipRoleDenyLogin { get; private set; }
        private string Authority { get; set;  }
        private string ClientId { get; set; }
        private string ClientSecret { get; set; }
        private string SignedOutRedirectUri { get; set; }

        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
            MembershipRole1 = Configuration["MembershipRole1"]?.ToString() ?? "";
            MembershipRole2 = Configuration["MembershipRole2"]?.ToString() ?? "";
            MembershipRole3 = Configuration["MembershipRole3"]?.ToString() ?? "";
            MembershipRoleDenyLogin = Configuration["MembershipRoleDenyLogin"]?.ToString() ?? "";
            Authority = Configuration["ServerUrl"]?.ToString() ?? "";
            ClientId = Configuration["ClientId"]?.ToString() ?? ""; ;
            ClientSecret = Configuration["ClientSecret"]?.ToString() ?? ""; ;
            SignedOutRedirectUri = Configuration["ReturnUrl"]?.ToString() ?? "";
        }


        public void ConfigureServices(IServiceCollection services)
        {
            JwtSecurityTokenHandler.DefaultInboundClaimFilter.Clear();

            services.AddMemoryCache();
            services.AddRazorPages();
            services.AddHttpClient();
            services.AddSession();

            services.Configure<ForwardedHeadersOptions>(options =>
            {
                options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
                options.KnownNetworks.Clear();
                options.KnownProxies.Clear();
            });

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie(configureOptions =>
            {
                configureOptions.ExpireTimeSpan = TimeSpan.FromMinutes(15);
                configureOptions.SlidingExpiration = true;
            })
            .AddOpenIdConnect("oidc", options => SetOpenIdConnectOptions(options));   


            services.AddControllersWithViews(ConfigureMvcOptions);

            services.AddHttpContextAccessor(); //Maybe needs this?
        }

        private void SetOpenIdConnectOptions(OpenIdConnectOptions options)
        {
            options.SaveTokens = true;
            options.Authority = Authority;
            options.ClientId = ClientId;
            options.ClientSecret = ClientSecret;
            options.SignedOutRedirectUri = SignedOutRedirectUri;
            options.ResponseType = "code id_token";
            options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; 

            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = ClaimTypes.Name,
                RoleClaimType = ClaimTypes.Role
            };
            options.GetClaimsFromUserInfoEndpoint = true;
            options.Scope.Clear();
            options.Scope.Add("openid");
            options.Scope.Add("MY.API");
            options.Scope.Add("offline_access");
            options.Scope.Add("profile");
    
            options.RequireHttpsMetadata = true;
            options.Events = new OpenIdConnectEvents
            {
                OnAuthorizationCodeReceived = async n =>
                {

                    TokenClientOptions myOptions = new()
                    {
                        ClientId = "local",  
                        ClientSecret = "******",
                        Address = "https://localhost:44318/connect/token" 
                    };

                    var client = new HttpClient();

                    var tokenClient = new TokenClient(client, myOptions);

                    var tokenResponse = await tokenClient.RequestAuthorizationCodeTokenAsync(n.TokenEndpointRequest.Code, n.TokenEndpointRequest.RedirectUri);

                    if (tokenResponse.IsError)
                    {
                        throw new Exception(tokenResponse.Error);
                    }


                    var userInfoResponse = await client.GetUserInfoAsync(new UserInfoRequest
                    {
                        Address = "https://localhost:44318/connect/userinfo", //UserInfoEndPoint ToDo: Access via appsettings
                        Token = tokenResponse.AccessToken
                    });

                    if(userInfoResponse.IsError)
                    {
                        throw new Exception("Problem while fetching data from the UserInfo endpoint", userInfoResponse.Exception);
                    }

                    Claim subject = userInfoResponse.Claims.FirstOrDefault(x => x.Type.Equals("sub"));


                    var id = new ClaimsIdentity(n.Principal.Identity.AuthenticationType);  

                    
                    id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
                    id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
                    id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
                    id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
                    id.AddClaim(new Claim("sid", n.Principal.FindFirst("sid").Value));  
                    id.AddClaim(new Claim("session_guid", string.Empty));

                  
                    ClaimsIdentity claimsIdentity = new ClaimsIdentity(id.Claims, n.Principal.Identity.AuthenticationType, ClaimTypes.Name, ClaimTypes.Role);

                    n.Principal.AddIdentity(claimsIdentity);

                },
                OnRedirectToIdentityProvider = n =>
                {
                    if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
                    {

                    }
                   
                    else if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                    {
                        var idTokenHint = n.HttpContext.User.FindFirst("id_token");//n.User.FindFirst("id_token");

                        if (idTokenHint != null)
                        {
                            n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                        }
                    }

                    return Task.FromResult(0);
                }
            };
            
        }
            
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {           

            app.UseCookiePolicy(new CookiePolicyOptions()
            {
                MinimumSameSitePolicy = SameSiteMode.Lax,
                Secure = CookieSecurePolicy.Always
            });

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseForwardedHeaders();
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthentication();      
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            }); 

            string showPII = Configuration["ShowPII"]?.ToLower() ?? "";
            if (showPII == "true")
            {
                IdentityModelEventSource.ShowPII = true;
            }

            JwtSecurityTokenHandler.DefaultInboundClaimFilter.Clear();

            app.UseSession();
            AppDomain.CurrentDomain.SetData("CurrentRootPath", env.ContentRootPath);
            AppDomain.CurrentDomain.SetData("WebRootPath", env.WebRootPath);

        }
}

I don't understand why the final 'authorization_code" GrantType is being called prior to the OnAuthorizationCodeReceived piece being called, I am, not calling any other request auths in my code either. All of the previous grants work just fine. For example, the log for var tokenResponse = await tokenClient.RequestAuthorizationCodeTokenAsync(n.TokenEndpointRequest.Code, n.TokenEndpointRequest.RedirectUri); shows it completing successfully.

UPDATE: Further up on the logs, I see where the same validation code that was not found in the database, was found earlier and then removed from persisted grant from database:

Start validation of authorization code token request
2022-09-26 14:16:53.855 -08:00 [DBG] 7EBFC1FBBB972962CACE378660D3E258C812B170DB496347289E19960C400FA9 found in database: true
2022-09-26 14:16:53.857 -08:00 [DBG] removing 7EBFC1FBBB972962CACE378660D3E258C812B170DB496347289E19960C400FA9 persisted grant from database
2022-09-26 14:16:53.885 -08:00 [DBG] Request path /.well-known/openid-configuration matched to endpoint type Discovery
2022-09-26 14:16:53.885 -08:00 [DBG] Endpoint enabled: Discovery, successfully created handler: IdentityServer4.Endpoints.DiscoveryEndpoint
2022-09-26 14:16:53.885 -08:00 [INF] Invoking IdentityServer endpoint: IdentityServer4.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration

CodePudding user response:

This particular error was occurring because I needed to call n.HandleCodeRedemption(access_token,id_token) at the end of OnAuthorizationCodeReceived. Once that was called, it no longer needed to request authorization, as I had already handled that with the RequestAuthorizationCodeTokenAsync code above.

On another note for anyone reading this with a similar issue, I also added a OnTokenValidated OpenIdConnectEvent, which basically handles the RequestAuthorizationCodeTokenAsync call and the adding of the claims.

  • Related