37

I am trying to redirect to IdentityServer for authorization, and getting "code challenge required" in redirect URL.

An error message shows invalid_request with code challenge required, and also my redirect url http://localhost:44367/signin-oidc#error=invalid_request&error_description=code%20challenge%20required&state=CfDJ8Cq6lLUEMhZLqMhFVN

Here is my client configuration:

namespace TestClient
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddControllersWithViews();

            ConfigureIdentityServer(services);
            services.AddCors();
        }

        private void ConfigureIdentityServer(IServiceCollection services)
        {
            var builder = services.AddAuthentication(options => SetAuthenticationOptions(options));
            services.AddMvcCore()
                .AddAuthorization();

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);

            builder.AddCookie();
            builder.AddOpenIdConnect(options => SetOpenIdConnectOptions(options));
        }

        private void SetAuthenticationOptions(AuthenticationOptions options)
        {
            options.DefaultScheme = Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectDefaults.AuthenticationScheme;
        }

        private void SetOpenIdConnectOptions(OpenIdConnectOptions options)
        {
            options.Authority = "https://localhost:44346";
            options.ClientId = "TestIdentityServer";
            options.RequireHttpsMetadata = false;
            options.Scope.Add("profile");
            options.Scope.Add("openid");
            options.Scope.Add("TestIdentityServer");
            options.ResponseType = "code id_token";
            options.SaveTokens = true;
            options.ClientSecret = "0b4168e4-2832-48ea-8fc8-7e4686b3620b";
        }


        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.

            }
            app.UseHsts();
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseCors(builder => builder
                .AllowAnyOrigin()
                .AllowAnyHeader()
                .AllowAnyMethod()
            );

            app.UseCookiePolicy();
            app.UseRouting();

            app.UseAuthentication();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

And here is my IdentityService4 configuration

  public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;

        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            IdentityModelEventSource.ShowPII = true;
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<ApplicationDbContext>();
            services.AddControllersWithViews();
            services.AddRazorPages();

            services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_3_0);
            services.Configure<IISOptions>(iis =>
            {
                iis.AuthenticationDisplayName = "Windows";
                iis.AutomaticAuthentication = false;
            });

            var builder = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;
            });
            // this adds the config data from DB (clients, resources)

            builder.AddInMemoryIdentityResources(Configuration.GetSection("IdentityResources"));
            builder.AddInMemoryApiResources(Configuration.GetSection("ApiResources"));
            builder.AddInMemoryClients(Configuration.GetSection("clients"));

            services.AddAuthentication();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.

            }
            app.UseHsts();
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();
            app.UseIdentityServer();

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

and appsettings.json

"IdentityResources": [
    {
      "Name": "openid",
      "DisplayName": "Your user identifier",
      "Required": true,
      "UserClaims": [
        "sub"
      ]
    },
    {
      "Name": "profile",
      "DisplayName": "User profile",
      "Description": "Your user profile information (first name, last name, etc.)",
      "Emphasize": true,
      "UserClaims": [
        "name",
        "family_name",
        "given_name",
        "middle_name",
        "preferred_username",
        "profile",
        "picture",
        "website",
        "gender",
        "birthdate",
        "zoneinfo",
        "locale",
        "updated_at"
      ]
    }
  ],

  "ApiResources": [
    {
      "Name": "TestIdentityServer",
      "DisplayName": "TestIdentityServer API Services",
      "Scopes": [
        {
          "Name": "TestIdentityServer",
          "DisplayName": "TestIdentityServer API Services"
        }
      ]
    }
  ],

  "Clients": [
    {
      "ClientId": "TestIdentityServer",
      "ClientName": "TestIdentityServer Credentials Client",

      // 511536EF-F270-4058-80CA-1C89C192F69A
      "ClientSecrets": [ { "Value": "entAuCGhsOQWRYBVx26BCgZxeMt/TqeVZzzpNJ9Ub1M=" } ],
      "AllowedGrantTypes": [ "hybrid" ],
      "AllowedScopes": [ "openid", "profile", "TestIdentityServer" ],
      "RedirectUris": [ "http://localhost:44367/signin-oidc" ],
      //"FrontChannelLogoutUris": [ "http://localhost:44367/Home/Privacy" ],
      //"PostLogoutRedirectUris": [ "http://localhost:44367/Home/Privacy" ],
      "redirect_uri": "http://localhost:44367/signin-oidc"
    }
Sérgio Azevedo
  • 313
  • 1
  • 4
  • 13
Shushan
  • 373
  • 1
  • 3
  • 5

5 Answers5

52

I am pretty much sure that you are using version 4.0 or above. Let me know if I am correct?

In version 4.0 and above, the code flow + PKCE is used by default, as this is more secure than Hybrid flow according to the documentation.

Here is the link https://identityserver4.readthedocs.io/en/latest/topics/grant_types.html and link to relevant issue on github https://github.com/IdentityServer/IdentityServer4/issues/3728 describing it as a breaking change.

I also struggled with it for about 2 hours when I upgraded IdentityServer4 package to the latest version in one of my projects.

If you want to use Hybrid flow set RequirePkce to false in your client configuration.

"Clients": {
   /* Code removed for brevity */
      RequirePkce : "false"
    }
Gurdev
  • 661
  • 5
  • 6
  • yes, in the newer version of IS4 we need to confirm the not use of Pkce in both (the client and the server) config. – Terai Feb 04 '21 at 17:47
16

Got that error today and solved it by switching from:

options.ResponseType = "code id_token";

to

options.ResponseType = "code";
options.UsePkce = true;

Here's my complete client-side options:

options.Authority = "http://localhost:8000";
options.RequireHttpsMetadata = false; // dev only

options.ClientId = "testAPI";
options.ClientSecret = secret;

// code flow + PKCE (PKCE is turned on by default)
options.ResponseType = "code";
options.UsePkce = true;

options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("offline_access");
options.Scope.Add("testAPI");

options.ClaimActions.MapJsonKey("website", "website");

//options.ResponseMode = "form_post";
//options.CallbackPath = "/signin-oidc";

// keeps id_token smaller
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;

Also, as I'm using IdentityServer on a docker and testing the client on the host, I had to configure an extra redirect Uri to be able to test:

RedirectUris =
{
    "http://localhost:5001/signin-oidc",
    "http://host.docker.internal:5001/signin-oidc",
    "http://notused"
},

I'm basing my implementation on Dominic Baier's samples on GitHub.

Edit: I've come to understand now that for my case the response type could only be "code" because my client configuration is for Authorization Code + PKCE (an OAuth2 flow). You have "Hybrid" configured (an OIDC flow) that supports "code id_token" so although we has received the same error message, the problem was different.

Sérgio Azevedo
  • 313
  • 1
  • 4
  • 13
1

try this : https://github.com/IdentityServer/IdentityServer4/issues/4238

set your RequirePkce as false in the configuration.

  • 2
    Welcome to stack overflow. Can you please explain what is in the link? Your answer must remain valid even if the link expires. – LuizZ Dec 05 '20 at 22:54
  • PKCE is critically important for security. I recommend that you re-enable it, unless you have a very specific reason for disabling it and understand your threat model. – Stuart Frankish Dec 19 '22 at 09:55
1

Run your application and once it is redirected to the browser page remove all the cookies Site Settings -> Under Usage-> Cookies -> Clear Data respective to that URL(https://localhost:5002) before redirection to login page. Then restart the application. This solved code challenge issue

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
         .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
         .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
         {
             options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
             options.Authority = "https://localhost:5005";
             options.ClientId = "movies_mvc_client";
             options.ClientSecret = "secret";
             options.ResponseType = "code";

             options.SaveTokens = true;
            // options.RequireHttpsMetadata = false;
             options.GetClaimsFromUserInfoEndpoint = true;
             options.Scope.Add("openid");
             options.Scope.Add("profile");
         });

Config.cs - Identity Server

                new Client
            {
             ClientId = "movies_mvc_client",
            ClientSecrets = { new Secret("secret".Sha256()) },
                
            AllowedGrantTypes = GrantTypes.Code,

             RedirectUris = { "https://localhost:5002/signin-oidc" },
            //    FrontChannelLogoutUri = "https://localhost:44300/signout-oidc",
             PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },

             AllowOfflineAccess = true,
             AllowedScopes = { "openid", "profile","movies_mvc_client"}
            }
1

Steps to fix this issue:

1- Set RequirePkce=true in new Client() in IdentityServer Config

2- In .AddOpenIdConnect() set options.UsePkce = true;

3- And add this in the same place:

 options.Events = new OpenIdConnectEvents
               {
                   OnRedirectToIdentityProvider = context =>
                   {
                       // Generate the code_verifier value
                       var codeVerifier = GenerateCodeVerifier();

                       // Store the code_verifier in a secure location (e.g. session)
                       context.Properties.Items.Add("code_verifier", codeVerifier);

                       // Hash the code_verifier to create the code_challenge value
                       var codeChallenge = GenerateCodeChallenge(codeVerifier);

                       // Add the code_challenge parameter to the authorization request
                       context.ProtocolMessage.SetParameter("code_challenge", codeChallenge);
                       context.ProtocolMessage.SetParameter("code_challenge_method", "S256");

                       return Task.CompletedTask;
                   }
               };

And here are the two methods which are used in this configuration:

 private static string GenerateCodeVerifier()
    {
        using (var rng = RandomNumberGenerator.Create())
        {
            var bytes = new byte[32];
            rng.GetBytes(bytes);
            return Base64Url.Encode(bytes);
        }
    }

    private static string GenerateCodeChallenge(string codeVerifier)
    {
        using (var sha256 = SHA256.Create())
        {
            var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
            return Base64Url.Encode(challengeBytes);
        }
    }
Hossein
  • 1,640
  • 2
  • 26
  • 41