Search code examples
c#authenticationjwtasp.net-core-mvcopenid-connect

InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync


My task is to do JWT authentication. My application is using .NET 6 and ASP NET MVC. I obtain a JWT access token during server-side authentication using OpenID Connect and then have it automatically added to the request headers for your authorized API calls.

My code in Program.cs:

const string AUTH_COOKIE_NAME = "access_token";
const string AUTH_SCHEMA_NAME = "OPENIDC_JWT";
const string BEARER_AUTH_SCHEMA_NAME = "bearer";
const string OIDC_AUTH_SCHEMA_NAME = "oidc";

builder.Services.AddAuthentication(options => {

    options.DefaultScheme = AUTH_SCHEMA_NAME;
    options.DefaultAuthenticateScheme = OIDC_AUTH_SCHEMA_NAME;
    options.DefaultChallengeScheme = OIDC_AUTH_SCHEMA_NAME;
})
    .AddOpenIdConnect(OIDC_AUTH_SCHEMA_NAME, o =>
    {
        o.MetadataAddress = builder.Configuration["SingleSignOn:MetaData"];
        o.Authority = builder.Configuration["SingleSignOn:Authority"];
        o.ClientId = builder.Configuration["SingleSignOn:ClientId"];
        o.ClientSecret = builder.Configuration["SingleSignOn:ClientSecret"];
        o.ResponseType = OpenIdConnectResponseType.Code;
        o.SaveTokens = true;
        o.Scope.Clear();
        o.Scope.Add("openid");
        o.Scope.Add("email");
        o.CallbackPath = "/signin-oidc";
        o.AccessDeniedPath = "/Account/AccessDenied";
        o.SignInScheme = BEARER_AUTH_SCHEMA_NAME;
        
        o.Events = new OpenIdConnectEvents
        {
            OnTokenValidated = async context =>
            {
                var accessToken = context.SecurityToken as JwtSecurityToken;
                if (accessToken != null)
                {
                    // Store the access token in the authentication properties
                    context.Properties.StoreTokens(new[]
                    {
                        new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = accessToken.RawData },
                    });
                }
               
                context.Request.Headers.Append("Authorization", $"Bearer {accessToken.RawData}");
                var roleService = context.HttpContext.RequestServices.GetRequiredService<IUserRolesService>();
                roleService.ManageUserRoles(context);
            },
        };
    })
    .AddJwtBearer(BEARER_AUTH_SCHEMA_NAME, options =>
    {
        options.MetadataAddress = "https://access-staging.epam.com/auth/realms/plusx/.well-known/openid-configuration";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true, // You shoud set ValidateAudience = true and specify ValidAudience for your application.
            ValidAudience = builder.Configuration["SingleSignOn:ClientId"],
            ValidateIssuerSigningKey = true,
        };
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME];

                // Token will be taken from Authorization header or if header is not set from authentication cookie.
                if (!string.IsNullOrEmpty(authCookie))
                {
                    context.Token = authCookie;
                }
                return Task.CompletedTask;
            }
        };
    })
    .AddPolicyScheme(AUTH_SCHEMA_NAME, AUTH_SCHEMA_NAME, options =>
    {
        options.ForwardDefaultSelector = context =>
        {
            string authHeader = context.Request.Headers[HeaderNames.Authorization];
            string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME];

            // If Token presents in header or cookie choose "bearer" schema, in other case use "oidc" schema.
            if (!string.IsNullOrEmpty(authHeader) || !string.IsNullOrEmpty(authCookie))
                return BEARER_AUTH_SCHEMA_NAME;

            return OIDC_AUTH_SCHEMA_NAME;
        };
    });

builder.Services.AddAuthorization();

// more services

var app = builder.Build();

AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();//HTTP Strict Transport Security Protocol is used to be sure that none of your content is still server over HTTP
}

app.UseHttpsRedirection();  
app.UseStaticFiles();
app.UseMiddleware<SecurityHeadersMiddleware>();
app.UseRouting();
app.UseAuthentication();

app.UseAuthorization();

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

app.Run();

As you can see from the code, I authenticate to my company via OpenIdConnect, receive a JWT access token and insert it into the headers for further communication with other services in my company via this JWT token. When a client needs to call protected API endpoints, it must include the JWT token in the Authorization header with the Bearer scheme, like this:

Authorization: Bearer your_jwt_token

Trace of the error: the context goes into the services, where, depending on the user’s email, they are assigned claims. Example of the service:

public void ManageUserRoles(TokenValidatedContext context)
{
    var user = context.Principal;

    if (user.Identity.IsAuthenticated)
    {
        if (!user.HasClaim(c => c.Type == ClaimTypes.Role))
        {
          if (!string.IsNullOrEmpty(userEmail) && isAdmin)
           {
             _logger.LogInformation($"Set the '{RoleNames.Admin}' role to user with email:  '{userEmail}'");
              identity.AddClaim(new Claim(ClaimTypes.Role, RoleNames.Admin));
            }

My error occurs just after I leave the event 'OnTokenValidated': InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT..

I am using AddPolicyScheme like handler in case if I have JWT token in header, I am using JWTBearer authentication, validate the token and go into my application on localhost; else if JWT token is null in the header, I must authenticate through my company's Identity provider, receive token, add it to the header, then validate it in 'AddJwtBearer' and authorize into my application.

Also to mention, I have the attribute [Authorize] on all my controllers. And do not offer me to use .AddCookie in authentication service or use cookies-based authentication schemes.

I tried using only Bearer authentication scheme as default:

builder.Services.AddAuthentication(options => {

    options.DefaultScheme = BEARER_AUTH_SCHEMA_NAME;
   
})

-- after that nothing is happening.

I tried to use different variations of schemes in authentication schemes:

options.DefaultScheme = AUTH_SCHEMA_NAME;
options.DefaultAuthenticateScheme = AUTH_SCHEMA_NAME;
options.DefaultChallengeScheme = AUTH_SCHEMA_NAME;

-- the same error after that: InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT..

Tried to use only one Default Scheme:

    options.DefaultScheme = AUTH_SCHEMA_NAME;

After that the same error: InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT..

Tried also this variant:

builder.Services.AddAuthentication(options => {

    options.DefaultScheme = BEARER_AUTH_SCHEMA_NAME;
    options.DefaultChallengeScheme = OIDC_AUTH_SCHEMA_NAME;
   
})

After that the same error: InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT..

Stack trace:

`System.InvalidOperationException: The authentication handler registered for scheme 'bearer' is 'JwtBearerHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: OPENIDC_JWT.

   at Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)

   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()

   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)

   at Middlewares.SecurityHeadersMiddleware.InvokeAsync(HttpContext context) in Middlewares\SecurityHeadersMiddleware.cs:line 23

   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)`

UPDATE: working sample for me is below. The problem of authentication schemes was resolved by setting in AddAuthentication options and specifying authentication schemes. You need to implement IClaimsTransformation interface to have built-in claims logic in authentication process if you need to do some custom claims for your identity.

 // Add services to the container.
builder.Services.AddTransient<IManageUsersRolesJwtContext, UserRolesService>();
builder.Services.AddTransient<IClaimsTransformation, ShipmentClaimsTransformation>();

// Define constants for authentication schemes and cookie name const string
const string AUTH_COOKIE_NAME = "access_token";
const string CUSTOM_POLICY_SCHEMA_NAME = "OIDC_JWT";
const string BEARER_AUTH_SCHEMA_NAME = "Bearer";
const string OIDC_AUTH_SCHEMA_NAME = "oidc";
const string METADATA_ADDRESS = "https://***/openid-configuration";

// Configure the cookie policy options
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    options.CheckConsentNeeded = context => true;
    options.MinimumSameSitePolicy = SameSiteMode.None;
});

// Configure the authentication services 
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CUSTOM_POLICY_SCHEMA_NAME;
    options.DefaultAuthenticateScheme = BEARER_AUTH_SCHEMA_NAME;
    options.DefaultChallengeScheme = OIDC_AUTH_SCHEMA_NAME;
})
    // Add custom policy scheme for handling bearer and OIDC authentication
    .AddOpenIdConnect(OIDC_AUTH_SCHEMA_NAME, o =>
    {
        o.MetadataAddress = builder.Configuration["SingleSignOn:MetaData"];
        o.Authority = builder.Configuration["SingleSignOn:Authority"];
        o.ClientId = builder.Configuration["SingleSignOn:ClientId"];
        o.ClientSecret = builder.Configuration["SingleSignOn:ClientSecret"];
        o.GetClaimsFromUserInfoEndpoint = true;
        o.ResponseType = "code";
        o.SaveTokens = true;
        o.Scope.Clear();
        o.Scope.Add("openid");
        o.Scope.Add("profile");
        o.Scope.Add("email");
        o.CallbackPath = "/signin-oidc";
        o.Events = new OpenIdConnectEvents
        {
            // Store access token in authentication properties when token is validated
            OnTokenValidated = context =>
            {
                var accessToken = context.SecurityToken as JwtSecurityToken;
                if (accessToken != null)
                {
                    // Store the access token in the authentication properties
                    context.Properties.StoreTokens(new[]
                    {
                        new AuthenticationToken { Name = OpenIdConnectParameterNames.AccessToken, Value = accessToken.RawData },
                    });
                }

                // Set the authentication cookie
                context.HttpContext.Response.Cookies.Append(AUTH_COOKIE_NAME, accessToken.RawData, new CookieOptions
                {
                    HttpOnly = true,
                    Secure = true,
                    SameSite = SameSiteMode.None,
                    Expires = context.SecurityToken.ValidTo.AddMinutes(-1)
                });
                context.Response.Headers.Add("Authorization", $"Bearer {accessToken.RawData}");
                // Redirect and handle response
                context.Response.Redirect(context.Properties.RedirectUri);
                context.HandleResponse();
                return Task.CompletedTask;
            }
        };
    })
    // Add JWT Bearer authentication
    .AddJwtBearer(BEARER_AUTH_SCHEMA_NAME, options =>
    {
        // Configure JWT options
        options.MetadataAddress = METADATA_ADDRESS;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration["SingleSignOn:Authority"],
            ValidateAudience = true, // You shoud set ValidateAudience = true and specify ValidAudience for your application.
            ValidAudience = builder.Configuration["SingleSignOn:ClientId"],
            ValidateIssuerSigningKey = true,
        };
        options.Events = new JwtBearerEvents
        {
            // Get the token from either the authorization header or authentication cookie
            OnMessageReceived = context =>
            {
                string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME];
                string authHeader = context.Request.Headers[HeaderNames.Authorization];

                // Token will be taken from Authorization header or if header is not set from authentication cookie.
                if (!string.IsNullOrEmpty(authCookie))
                {
                     context.Token = authCookie;
                }
                return Task.CompletedTask;
            }
        };
    }).AddPolicyScheme(CUSTOM_POLICY_SCHEMA_NAME, CUSTOM_POLICY_SCHEMA_NAME, options =>
    {
        options.ForwardDefaultSelector = context =>
        {
            var isAuthenticated = context.User?.Identity?.IsAuthenticated ?? false;

            string authHeader = context.Request.Headers[HeaderNames.Authorization];
            string authCookie = context.Request.Cookies[AUTH_COOKIE_NAME];

            // If Token presents in the header or cookie choose "bearer" schema, in other case use "oidc" schema.
            if (!string.IsNullOrEmpty(authHeader) || !string.IsNullOrEmpty(authCookie))
                return BEARER_AUTH_SCHEMA_NAME;
            else
                return OIDC_AUTH_SCHEMA_NAME;
        };
    });

builder.Services.AddAuthorization();

    ```

Solution

  • The JwtBearer handler does not implement SignIn or Signout of a user. To sign in the user you typically do that do that directly to the cookie handler or let the OpenIDConnect handler do it. JwtBearer only accepts access tokens and converts it to a ClaimsPrincipal user, that is its only purpose.

    The image below (taken from one my my training classes) compares the cookie and JwtBearer handler:

    enter image description here