ISAAC POULTON ISAAC POULTON - 13 days ago 9
ASP.NET (C#) Question

ASP.NET JWT bearer authentication not changing user in request context

I'm creating a simple OAuth server for my SPA, following along with this tutorial. I can request a JWT fine from the OAuth provider. However, when I send the JWT along in the authorization header (like this:

Authorization Bearer <token>
) I always get a 401 "Authorization has been denied for this request".

Stepping through the code, I can see that my user in the request context is set to an anonymous Windows authentication user instead of the owner of the JWT.

I see a lot of people saying to put the
app.UseWebAPI()
call after the OAuth configuration, but I'm not registering the WebAPI in my startup.cs. Should I be? I currently don't have an
app.UseWebAPI()
anywhere in my code (and the API works fine without the authorization).

Here's my startup.cs:

public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureOAuth(app);
}
}

public partial class Startup
{
/// <summary>
/// OAuth configuration
/// </summary>
/// <param name="app"></param>
public void ConfigureOAuth(IAppBuilder app)
{
// Unique identifier for the entity issuing the token
var issuer = ConfigurationManager.AppSettings["issuer"];
// Private key used to secure the token
var secret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["secret"]);

// Enable JWT authentication
app.UseJwtBearerAuthentication(new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { "Any" },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, secret)
}
});

// Create authentication endponit
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/api/login/"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
Provider = new CustomOAuthProvider(),
AccessTokenFormat = new CustomJwtFormat(issuer)
});
}
}


Here's my CustomOAuthProvider class:

public class CustomOAuthProvider : OAuthAuthorizationServerProvider
{
IMessengerRepository repo;

public CustomOAuthProvider() : base()
{
repo = new MessengerRepository();
}

public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// Enable CORS
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });

// Check against the repository to see if the login details are correct
var user = repo.Authenticate(context.UserName, context.Password);

// If the login details are incorrect, return an error
if (user == null)
{
context.SetError("invalid_grant", "The username or password is incorrect");
context.Rejected();
return Task.FromResult<object>(null);
}

// Otherwise, return a JWT
var ticket = new AuthenticationTicket(SetClaimsIdentity(context, user), new AuthenticationProperties());
context.Validated(ticket);

return Task.FromResult<object>(null);
}

public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
// Because the audience is not validated, the request is validated straight away without any checks
context.Validated();
return Task.FromResult<object>(null);
}

/// <summary>
/// Create a ClaimsIdentity based on their username and role
/// </summary>
/// <param name="context"></param>
/// <param name="user"></param>
/// <returns></returns>
public static ClaimsIdentity SetClaimsIdentity(OAuthGrantResourceOwnerCredentialsContext context, UserDto user)
{
var identity = new ClaimsIdentity("JWT");

// Add claims for the user themselves
identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
identity.AddClaim(new Claim("sub", context.UserName));
// Add claim for the user's role
identity.AddClaim(new Claim(ClaimTypes.Role, user.UserRole.RoleName.TrimEnd(' ')));

return identity;
}
}


and my CustomJwtFormat class:

public class CustomJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
// Private key for encoding token
private static readonly byte[] secret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["secret"]);
// Entity issuing the token
private readonly string issuer;

public CustomJwtFormat(string issuer)
{
this.issuer = issuer;
}

public string Protect(AuthenticationTicket data)
{
if (data == null)
throw new ArgumentNullException(nameof(data));

var signingKey = new HmacSigningCredentials(secret);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;

return new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(issuer, null, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey));
}

public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
}

Answer

Isaac,

Your implementation looks good, but there is a tiny problem with the audience id.

In your CustomJwtFormat for authorization you specify null as an audience id. While for authentication you specify Any. This is the reason why you get a 401 Unauthorized.

If you change your JwtSecurityToken with the following it will work.

return new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(issuer, "Any", data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey));