5
votes

I am using Identity Server 4 .Net Core 3, my API endpoint does not validate access token if I use standard configuration in startup, I keep getting 401 Unauthorized, however when I set the authentication scheme in the controller with the authorize property, I can successfully access my endpoint with the same token...

[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiController]
public class MyWebAPiControllerController : ControllerBase
{
.......

Here is my Identity Server Config:

//API resource       
public IEnumerable<ApiResource> Apis()
{
        var resources = new List<ApiResource>();

        resources.Add(new ApiResource("identity", "My API", new[] { JwtClaimTypes.Subject, JwtClaimTypes.Email, JwtClaimTypes.Role, JwtClaimTypes.Profile }));

        return resources;
}

My Client configuration:

public IEnumerable<Client> Clients()
    {

        var Clients = new List<Client>();

        Clients.Add(new Client
        {
            ClientId = "client",
            ClientSecrets = { new Secret(_securityConfig.Secret.Sha256()) },

            AllowedGrantTypes = GrantTypes.ClientCredentials,
            // scopes that client has access to
            AllowedScopes = { "identity" }
        });

        Clients.Add(new Client
        {
            ClientId = "mvc",
            ClientName = "MVC Client",

            AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
            //RequirePkce = true,
            ClientSecrets = { new Secret(_securityConfig.Secret.Sha256()) },
            RequireConsent = false,
            RedirectUris = _securityConfig.RedirectURIs,
            FrontChannelLogoutUri = _securityConfig.SignoutUris,
            PostLogoutRedirectUris = _securityConfig.PostLogoutUris,
            AllowOfflineAccess = true,
            AllowAccessTokensViaBrowser = true,
            AllowedScopes = new List<string>
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    IdentityServerConstants.StandardScopes.Email,
                    IdentityServerConstants.StandardScopes.OfflineAccess,
                    "identity"
                }

        });

        return Clients;
    } 

My API Configuration

 services.AddAuthentication("Bearer")
            .AddJwtBearer("Bearer", options =>
            {
                options.Authority = _securityConfig.Authority;
                options.RequireHttpsMetadata = false;

                options.Audience = "identity";
            });

and Finally my web app, OIDC configuration and how I get the access token:

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = "oidc";
        }).AddCookie(options =>
            {
                options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
                options.Cookie.Name = "identity_cookie";
            })
        .AddOpenIdConnect("oidc", options =>
        {
            options.Events = new OpenIdConnectEvents
            {
                OnUserInformationReceived = async ctx =>
                {
                    //Get Token here and assign to Cookie for use in Jquery
                    ctx.HttpContext.Response.Cookies.Append("bearer_config", ctx.ProtocolMessage.AccessToken);
                }
            };

            options.Authority = _securityConfig.Authority;
            options.RequireHttpsMetadata = false;

            options.ClientId = "mvc";
            options.ClientSecret = _securityConfig.Secret;
            options.ResponseType = "code id_token";
            options.SaveTokens = true;


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

            options.ClaimActions.MapAllExcept("iss", "nbf", "exp", "aud", "nonce", "iat", "c_hash");

            options.GetClaimsFromUserInfoEndpoint = true;
            //options.SaveTokens = true;

            options.TokenValidationParameters = new TokenValidationParameters
            {
                NameClaimType = JwtClaimTypes.Name,
                RoleClaimType = JwtClaimTypes.Role,
            };


        });

Any ideas as to why I keep getting 401 Unauthorized?

1
Can you verify the order of the middleware in Startup.Configure? And in particular the UseAuthorization statement.Ruard van Elburg
@RuardvanElburg, My middleware is as follows: app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseAuthentication();Jacques Bronkhorst
It's the other way around, UseAuthentication then UseAuthorization. First identify the user then authorize the user. That's why the middleware doesn't work, but the attributes do.Ruard van Elburg
@RuardvanElburg It worked! Just add it as an answer and I'll accept it. You sir are a Scholar and a gentleman!Jacques Bronkhorst
This can also happen if you're stupid enough to copy and paste app.UseAuthorization(); twice, instead of having app.UseAuthentication() like my sorry a.. Took me hours to realize that.GeorgiG

1 Answers

4
votes

Based on the the described behaviour, I thought it could have to do with the middleware configuration, more specifically the order of middleware. But I couldn't be sure since the Startup.Configure wasn't available in the question.

Luckily Jacques could confirm that the problem was indeed with the order. As mentioned in the comment:

app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();

The problem here is that the user is first authorized (UseAuthorization) and then authenticated (UseAuthentication). So the user can never be authorized as it is unknown (anonymous) at that point. But later on, when the attributes are validated, the user is known. So that explains why that works.

In order to solve this problem, the statements have to be switched. First authenticate the user (identify the user, who is the user?) and then authorize the user:

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

The order is described in the documentation.