Identity Server 4 with ASP.NET 4.6.2 Web API causes redirect

I am trying to use Identity Server 4 with my Web API written in .NET Framework 4.6.2. I am using the IdentityServer3.Contrib.AccessTokenValidation library as mentioned in this answer. But, the calls to the protected endpoints are returning a 402 redirect.

Here’s the code from the API:

private void ConfigureAuth(IAppBuilder app)
        {
            app.Map("/api",
                    apiApp =>
                    {
                        apiApp.UseCors(corsOptions);
                        apiApp.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
                        {
                            Authority = apiAuthority,
                            ValidationMode = ValidationMode.ValidationEndpoint,
                            RequiredScopes = new[] { "api" }
                        });
                        SetupOpenIdAuthentication(apiApp);
                    });
        }

        private static void SetupOpenIdAuthentication(IAppBuilder app)
        {
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "Cookies",
                ExpireTimeSpan = new TimeSpan(0, Configuration.SessionTimeoutInMinutes, 0),
                SlidingExpiration = true
            });
            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                Authority = apiAuthority,
                ClientId = "clientId",
                RedirectUri = apiRootUri + "Help",
                PostLogoutRedirectUri = apiRootUri,
                ResponseType = "id_token token",
                Scope = "openid profile roles api all_claims",
                SignInAsAuthenticationType = "Cookies",
                Notifications = new OpenIdConnectAuthenticationNotifications()
                {
                    RedirectToIdentityProvider = n =>
                    {
                        // if signing out, add the id_token_hint
                        if (n.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Logout)
                        {
                            var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
                            if (idTokenHint != null)
                            {
                                n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                            }
                        }

                        if (n.ProtocolMessage.RequestType == Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectRequestType.Authentication)
                        {
                            if (IsAjaxRequest(n.Request) && n.Response.StatusCode == (int)System.Net.HttpStatusCode.Unauthorized)
                            {
                                n.Response.StatusCode = (int)System.Net.HttpStatusCode.Unauthorized;
                                n.HandleResponse();
                                return Task.FromResult(0);
                            }
                        }

                        return Task.FromResult(0);
                    },
                    SecurityTokenValidated = n =>
                    {
                        // keep the id_token for logout
                        n.AuthenticationTicket.Identity.AddClaim(
                            new Claim("id_token", n.ProtocolMessage.IdToken));

                        //Add Role claims as MS claims so Authorize works on API methods when used without the bearer token
                        foreach (var claim in n.AuthenticationTicket.Identity.Claims.Where(x => x.Type == JwtClaimTypes.Role).ToList())
                        {
                            n.AuthenticationTicket.Identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
                        }

                        return Task.FromResult(0);
                    },
                    AuthenticationFailed = n =>
                    {
                        // Pass in the context back to the app
                        n.OwinContext.Response.Redirect("/Help/Error");
                        //TODO:  Create authentication failure page
                        n.HandleResponse(); // Suppress the exception
                        return Task.FromResult(0);
                    }
                }
            });
        }

Code from Identity Server:

var builder = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;

                // see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
                options.EmitStaticAudienceClaim = true;
            })
            .AddSigningCredential(rsaCertificate)
            .AddInMemoryIdentityResources(IdentityConfig.IdentityResources)
            .AddInMemoryApiScopes(IdentityConfig.ApiScopes)
            .AddInMemoryClients(IdentityConfig.Clients)
            .AddAspNetIdentity<User>()
            .AddProfileService<CustomProfileService>()
            .AddWsFederationPlugin(options =>
            {
                options.Licensee = "License";
                options.LicenseKey = "Key"
            })
            .AddInMemoryRelyingParties(new List<RelyingParty>());

            services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
            })
            .AddWsFederation(options =>
            {
                options.Wtrealm = "azureAppId";
                options.MetadataAddress = "metadataAddress";
            });
            builder.Services.ConfigureApplicationCookie(options =>
                {
                    options.Cookie.IsEssential = true;
                    options.Cookie.SameSite = SameSiteMode.None; //SameSiteMode.Unspecified in .NET Core 3.1
                });

The following is the client registered for the API in Identity Server:

new Client
            {
                ClientId = "clientId",
                AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials,
                ClientSecrets = { new Secret("secret".Sha256()) },
                RedirectUris = { "https://localhost:44302/", $"{apiUrl}/Help" },
                PostLogoutRedirectUris = { $"{apiUrl}" },
                AllowedScopes = new List<string>
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "api",
                    "roles",
                    "all_claims"
                },
                RequirePkce = false,
                AllowOfflineAccess = true,
                AllowAccessTokensViaBrowser = true
            }

Please let me know if I need to add more code. The same access token when used to call a .NET Core API protected with Identity Server 4 will not cause a redirect. But, when used with an API in .NET 4.6.2, it causes a redirect.

Here is Solutions:

We have many solutions to this problem, But we recommend you to use the first solution because it is tested & true solution that will 100% work for you.

Solution 1

In the API, change ValidationMode = ValidationMode.ValidationEndpoint to ValidationMode = ValidationMode.Both. Which would enable Identity Server to use local validation for JWTs and validation endpoint for reference tokens.

Note: Use and implement solution 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply