Reputation:
I have the following problem:
The web api uses JWT to authorize people. I have been following this tutorial: here
The token provider works fine as shown on the postman picture:
But when I try to pass the token in postman to the following controller:
[Authorize]
[Route("ChangePassword")]
public async Task<IHttpActionResult> ChangePassword(ChangePasswordBindingModel model) {
if (!ModelState.IsValid) {
return BadRequest(ModelState);
}
IdentityResult result = await this.AppUserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword, model.NewPassword);
if (!result.Succeeded)
return GetErrorResult(result);
return Ok();
}
Then this will be the result:
I can't see what the problem should be. I do in the startup file also start the API last.
public class Startup {
public void Configuration(IAppBuilder app) {
HttpConfiguration httpConfig = new HttpConfiguration();
ConfigureOAuthTokenGeneration(app);
ConfigureOAuthTokenConsumption(app);
ConfigureWebApi(httpConfig);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(httpConfig);
}
private void ConfigureWebApi(HttpConfiguration httpConfig) {
httpConfig.MapHttpAttributeRoutes();
var jsonFormatter = httpConfig.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
private void ConfigureOAuthTokenGeneration(IAppBuilder app) {
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions() {
//Set to false in production
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new CustomOAuthProvider(),
AccessTokenFormat = new CustomJwtFormat("http://localhost:44300")
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
}
private void ConfigureOAuthTokenConsumption(IAppBuilder app) {
var issuer = "http://localhost:44300";
var audienceId = "414e1927a3884f68abc79f7283837fd1";
var audienceSecret = TextEncodings.Base64Url.Decode("qMCdFDQuF23RV1Y-1Gq9L3cF3VmuFwVbam4fMTdAfpo");
// Api controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audienceId },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret)
}
});
}
}
Upvotes: 0
Views: 1463
Reputation: 10068
I think I figured out the problem with both tutorials (this and this). Take a look at CustomJwtFormat
class Protect()
method
public string Protect(AuthenticationTicket data) {
...
var issued = data.Properties.IssuedUtc;
...
var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims,
issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
...
}
It looks like JwtSecurityToken
is expecting notBefore
parameter in local and not in UTC timezone. If the author is in Jordan (UTC+2), it will mean that notBefore will be 2 hours earlier than now, but it will still work. However, since I am in California (UTC-8), notBefore is set to 8 hours later and token validation fails! The solution is to have
DateTimeOffset issued = data.Properties.IssuedUtc?.ToLocalTime();
in the new C# 6 format, or
DateTimeOffset issued = data.Properties.IssuedUtc.Value.ToLocalTime()
using classic syntax. Thank you @Treetopvt for pushing me to debug through middleware. With this change standard JwtSecurityTokenHandler
works fine
Upvotes: 0
Reputation: 240
I've recently went through the same tutorial and had a similar problem. All endpoints with an [Authorize] attribute were returning 401. I completely broke apart the JwtBearerAuthentication middleware and discovered a problem with the JWTSecurityTokenHandler determining whether an audience was valid.
For starters, as most guides will tell you, verify your audience, issuer, and secret are the same where you generate your JWT token and where you ConfigureOAuthConsumption. I found it was easy to confuse these on the JWT creation side. If they are both correct look at the code below.
I ended up creating my own JWT Handler which derives from JwtSecurityTokenHandler. It mostly just calls the base methods, but it does give you great insight into how the validation process works. Please note the code change in ValidateToken.
class CustomJWTTokenHandler : JwtSecurityTokenHandler
{
public CustomJWTTokenHandler():base()
{
}
public override bool CanReadToken(string tokenString)
{
var rtn = base.CanReadToken(tokenString);
return rtn;
}
public override bool CanValidateToken
{
get
{
return base.CanValidateToken;
}
}
protected override ClaimsIdentity CreateClaimsIdentity(JwtSecurityToken jwt, string issuer, TokenValidationParameters validationParameters)
{
return base.CreateClaimsIdentity(jwt, issuer, validationParameters);
}
public override ReadOnlyCollection<ClaimsIdentity> ValidateToken(SecurityToken token)
{
try
{
var rtn = base.ValidateToken(token);
return rtn;
}
catch (Exception)
{
throw;
}
}
public override ClaimsPrincipal ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
{
var jwt = this.ValidateSignature(securityToken, validationParameters);
if (validationParameters.ValidateAudience)
{
if (validationParameters.AudienceValidator != null)
{
if (!validationParameters.AudienceValidator(jwt.Audiences, jwt, validationParameters))
{
throw new SecurityTokenInvalidAudienceException(string.Format(CultureInfo.InvariantCulture, ErrorMessages.IDX10231, jwt.ToString()));
}
}
else
{
base.ValidateAudience(validationParameters.ValidAudiences, jwt, validationParameters);
}
}
string issuer = jwt.Issuer;
if (validationParameters.ValidateIssuer)
{
if (validationParameters.IssuerValidator != null)
{
issuer = validationParameters.IssuerValidator(issuer, jwt, validationParameters);
}
else
{
issuer = ValidateIssuer(issuer, jwt, validationParameters);
}
}
if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jwt.Actor))
{
SecurityToken actor = null;
ValidateToken(jwt.Actor, validationParameters, out actor);
}
ClaimsIdentity identity = this.CreateClaimsIdentity(jwt, issuer, validationParameters);
if (validationParameters.SaveSigninToken)
{
identity.BootstrapContext = new BootstrapContext(securityToken);
}
validatedToken = jwt;
return new ClaimsPrincipal(identity);
}
protected override JwtSecurityToken ValidateSignature(string token, TokenValidationParameters validationParameters)
{
var rtn = base.ValidateSignature(token, validationParameters);
var issuer = rtn.Issuer;
return rtn;
}
protected override void ValidateAudience(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
if (audiences !=null && audiences.Any())
{
var jwt = securityToken as JwtSecurityToken;
if (!jwt.Audiences.Any())
{
throw new Exception("token has no audiences defined");
}
var inBothList= audiences.Where(X => jwt.Audiences.Contains(X)).ToList();
if (!inBothList.Any()){
throw new Exception("token not in audience list");
}
}
//base.ValidateAudience(audiences, securityToken, validationParameters);
}
public override SecurityToken ReadToken(string tokenString)
{
var rtnToken = base.ReadToken(tokenString);
//var validations = this.ValidateToken(rtnToken);
return rtnToken;
}
}
This handler is wired up when you setup the UseJwtBearerAuthentication middleware:
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,
AllowedAudiences = new List<string>() { JWTConfigs.audience },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new CustomSymmetricKeyIssuerSecurityTokenProvider(JWTConfigs.issuer, key)
},
TokenHandler = new CustomJWTTokenHandler()
}
);
Hopefully this works for you or at least points you to why your Token is failing.
Upvotes: 2