Search code examples
angularjsoauth-2.0asp.net-coreopenid-connectaspnet-contrib

Asp.net core token based claims authentication with OpenIdConnect and angularjs: Bearer was forbidden


I'm using Asp.net core rc2 with OpenIdConnectServer. I'm using angular 1.x with augular-oauth2. After a few days, my error has digressed to

Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:54275/api/Account/Username  
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: Successfully validated the token.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: HttpContext.User merged via AutomaticAuthentication from authenticationScheme: Bearer.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: AuthenticationScheme: Bearer was successfully authenticated.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService:Information: Authorization failed for user: .
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker:Warning: Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
Microsoft.AspNetCore.Mvc.ChallengeResult:Information: Executing ChallengeResult with authentication schemes (Bearer).
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerMiddleware:Information: AuthenticationScheme: Bearer was forbidden.

My ConfigureServices consists of

services.AddAuthorization(options =>
            {
                options.AddPolicy("UsersOnly", policy =>
                {
                    policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
                    policy.RequireClaim("role");
                });
            });

My configure has

app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
            {
                branch.UseJwtBearerAuthentication(new JwtBearerOptions
                {
                    AutomaticAuthenticate = true,
                    AutomaticChallenge = true,
                    RequireHttpsMetadata = false,

                    Audience = "http://localhost:54275/",
                    Authority = "http://localhost:54275/",
                    TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidAudience = "client1",
                        //ValidAudiences = new List<string> { "", "empty", "null"}
                    }
                });
            });

            app.UseOpenIdConnectServer(options =>
            {
                options.AuthenticationScheme = OpenIdConnectServerDefaults.AuthenticationScheme;
                options.Provider = new SimpleAuthorizationServerProvider();
                options.AccessTokenHandler = new JwtSecurityTokenHandler();
                options.ApplicationCanDisplayErrors = true;
                options.AllowInsecureHttp = true;
                options.TokenEndpointPath = new PathString("/oauth2/token");
                options.LogoutEndpointPath = new PathString("/oauth2/logout");
                options.RevocationEndpointPath = new PathString("/oauth2/revoke");
                options.UseJwtTokens();
                //options.AccessTokenLifetime = TimeSpan.FromHours(1);
            });

My authorize attribute is defined on the Controller as

[Authorize(Policy = "UsersOnly", ActiveAuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme), Route("api/Account")]

I store the token as a cookie and attach it to requests using an http interceptor in angular.

I generate the token with

public override async Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsContext context)
        {
            // validate user credentials (demo mode)
            // should be stored securely (salted, hashed, iterated)
            using (var con = new SqlConnection(ConnectionManager.GetDefaultConnectionString()))
            {
                if (!Hashing.ValidatePassword(context.Password, await con.ExecuteScalarAsync<string>("SELECT PassHash FROM dbo.Users WHERE Username = @UserName", new { context.UserName })))
                {
                    context.Reject(
                        error: "bad_userpass",
                        description: "UserName/Password combination was invalid."
                        );
                    return;
                }

                // create identity
                var id = new ClaimsIdentity(context.Options.AuthenticationScheme);
                id.AddClaim(new Claim("sub", context.UserName));
                id.AddClaim(new Claim("role", "user"));

                // create metadata to pass on to refresh token provider
                var props = new AuthenticationProperties(new Dictionary<string, string>
                {
                    {"as:client_id", context.ClientId}
                });
                var ticket = new AuthenticationTicket(new ClaimsPrincipal(id), props,
                    context.Options.AuthenticationScheme);
                ticket.SetAudiences("client1");
                //ticket.SetScopes(OpenIdConnectConstants.Scopes.OpenId, OpenIdConnectConstants.Scopes.Email, OpenIdConnectConstants.Scopes.Profile, "api-resource-controller");
                context.Validate(ticket);
            }
        }

I've spent the last three days on this problem and I realize that at this point I'm probably missing something obvious due to lack of sleep. Any help would be appreciated.


Solution

  • The error you're seeing is likely caused by 2 factors:

    • You're not attaching an explicit destination to your custom role claim so it will never be serialized in the access token. You can find more information about this security feature on this other SO post.

    • policy.RequireClaim("role"); might not work OTB, as IdentityModel uses an internal mapping that converts well-known JWT claims to their ClaimTypes equivalent: here, role will be likely replaced by http://schemas.microsoft.com/ws/2008/06/identity/claims/role (ClaimTypes.Role). I'd recommend using policy.RequireRole("user") instead.

    It's also worth noting that manually storing the client_id is not necessary as it's already done for you by the OpenID Connect server middleware.

    You can retrieve it using ticket.GetPresenters(), that returns the list of authorized presenters (here, the client identifier). Note that it also automatically ensures a refresh token issued to a client A can't be used by a client B, so you don't have to do this check in your own code.