The UseJwtBearerAuthentication middleware in ASP.NET Core makes it easy to validate incoming JSON Web Tokens in Authorization headers.
How do I authenticate a JWT passed via cookies, instead of a header? Something like UseCookieAuthentication, but for a cookie that just contains a JWT.
I suggest you take a look at the following link.
https://stormpath.com/blog/token-authentication-asp-net-core
They store JWT token in an http only cookie to prevent XSS attacks.
They then validate the JWT token in the cookie by adding the following code in the Startup.cs:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = "Cookie",
CookieName = "access_token",
TicketDataFormat = new CustomJwtDataFormat(
SecurityAlgorithms.HmacSha256,
tokenValidationParameters)
});
Where CustomJwtDataFormat() is their custom format defined here:
public class CustomJwtDataFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly string algorithm;
private readonly TokenValidationParameters validationParameters;
public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters)
{
this.algorithm = algorithm;
this.validationParameters = validationParameters;
}
public AuthenticationTicket Unprotect(string protectedText)
=> Unprotect(protectedText, null);
public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
var handler = new JwtSecurityTokenHandler();
ClaimsPrincipal principal = null;
SecurityToken validToken = null;
try
{
principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken);
var validJwt = validToken as JwtSecurityToken;
if (validJwt == null)
{
throw new ArgumentException("Invalid JWT");
}
if (!validJwt.Header.Alg.Equals(algorithm, StringComparison.Ordinal))
{
throw new ArgumentException($"Algorithm must be '{algorithm}'");
}
// Additional custom validation of JWT claims here (if any)
}
catch (SecurityTokenValidationException)
{
return null;
}
catch (ArgumentException)
{
return null;
}
// Validation passed. Return a valid AuthenticationTicket:
return new AuthenticationTicket(principal, new AuthenticationProperties(), "Cookie");
}
// This ISecureDataFormat implementation is decode-only
public string Protect(AuthenticationTicket data)
{
throw new NotImplementedException();
}
public string Protect(AuthenticationTicket data, string purpose)
{
throw new NotImplementedException();
}
}
Another solution would be to write some custom middleware that would intercept each request, look if it has a cookie, extract the JWT from the cookie and add an Authorization header on the fly before it reaches the Authorize filter of your controllers. Here is some code that work for OAuth tokens, to get the idea:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace MiddlewareSample
{
public class JWTInHeaderMiddleware
{
private readonly RequestDelegate _next;
public JWTInHeaderMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
var authenticationCookieName = "access_token";
var cookie = context.Request.Cookies[authenticationCookieName];
if (cookie != null)
{
var token = JsonConvert.DeserializeObject<AccessToken>(cookie);
context.Request.Headers.Append("Authorization", "Bearer " + token.access_token);
}
await _next.Invoke(context);
}
}
}
... where AccessToken is the following class:
public class AccessToken
{
public string token_type { get; set; }
public string access_token { get; set; }
public string expires_in { get; set; }
}
Hope this helps.
NOTE: It is also important to note that this way of doing things (token in http only cookie) will help prevent XSS attacks but however does not immune against Cross Site Request Forgery (CSRF) attacks, you must therefore also use anti-forgery tokens or set custom headers to prevent those.
Moreover, if you do not do any content sanitization, an attacker can still run an XSS script to make requests on behalf of the user, even with http only cookies and CRSF protection enabled. However, the attacker will not be able to steal the http only cookies that contain the tokens, nor will the attacker be able to make requests from a third party website.
You should therefore still perform heavy sanitization on user-generated content such as comments etc...
EDIT: It was written in the comments that the blog post linked and the code have been written by the OP himself a few days ago after asking this question.
For those who are interested in another "token in a cookie" approach to reduce XSS exposure they can use oAuth middleware such as the OpenId Connect Server in ASP.NET Core.
In the method of the token provider that is invoked to send the token back (ApplyTokenResponse()) to the client you can serialize the token and store it into a cookie that is http only:
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server;
using Newtonsoft.Json;
namespace Shared.Providers
{
public class AuthenticationProvider : OpenIdConnectServerProvider
{
private readonly IApplicationService _applicationservice;
private readonly IUserService _userService;
public AuthenticationProvider(IUserService userService,
IApplicationService applicationservice)
{
_applicationservice = applicationservice;
_userService = userService;
}
public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
{
if (string.IsNullOrEmpty(context.ClientId))
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Missing credentials: ensure that your credentials were correctly " +
"flowed in the request body or in the authorization header");
return Task.FromResult(0);
}
#region Validate Client
var application = _applicationservice.GetByClientId(context.ClientId);
if (applicationResult == null)
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Application not found in the database: ensure that your client_id is correct");
return Task.FromResult(0);
}
else
{
var application = applicationResult.Data;
if (application.ApplicationType == (int)ApplicationTypes.JavaScript)
{
// Note: the context is marked as skipped instead of validated because the client
// is not trusted (JavaScript applications cannot keep their credentials secret).
context.Skip();
}
else
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidClient,
description: "Authorization server only handles Javascript application.");
return Task.FromResult(0);
}
}
#endregion Validate Client
return Task.FromResult(0);
}
public override async Task HandleTokenRequest(HandleTokenRequestContext context)
{
if (context.Request.IsPasswordGrantType())
{
var username = context.Request.Username.ToLowerInvariant();
var user = await _userService.GetUserLoginDtoAsync(
// filter
u => u.UserName == username
);
if (user == null)
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid username or password.");
return;
}
var password = context.Request.Password;
var passWordCheckResult = await _userService.CheckUserPasswordAsync(user, context.Request.Password);
if (!passWordCheckResult)
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid username or password.");
return;
}
var roles = await _userService.GetUserRolesAsync(user);
if (!roles.Any())
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidRequest,
description: "Invalid user configuration.");
return;
}
// add the claims
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(ClaimTypes.NameIdentifier, user.Id, OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
identity.AddClaim(ClaimTypes.Name, user.UserName, OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
// add the user's roles as claims
foreach (var role in roles)
{
identity.AddClaim(ClaimTypes.Role, role, OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
}
context.Validate(new ClaimsPrincipal(identity));
}
else
{
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Invalid grant type.");
return;
}
return;
}
public override Task ApplyTokenResponse(ApplyTokenResponseContext context)
{
var token = context.Response.Root;
var stringified = JsonConvert.SerializeObject(token);
// the token will be stored in a cookie on the client
context.HttpContext.Response.Cookies.Append(
"exampleToken",
stringified,
new Microsoft.AspNetCore.Http.CookieOptions()
{
Path = "/",
HttpOnly = true, // to prevent XSS
Secure = false, // set to true in production
Expires = // your token life time
}
);
return base.ApplyTokenResponse(context);
}
}
}
Then you need to make sure each request has the cookie attached to it. You must also write some middleware to intercept the cookie and set it to the header:
public class AuthorizationHeader
{
private readonly RequestDelegate _next;
public AuthorizationHeader(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
var authenticationCookieName = "exampleToken";
var cookie = context.Request.Cookies[authenticationCookieName];
if (cookie != null)
{
if (!context.Request.Path.ToString().ToLower().Contains("/account/logout"))
{
if (!string.IsNullOrEmpty(cookie))
{
var token = JsonConvert.DeserializeObject<AccessToken>(cookie);
if (token != null)
{
var headerValue = "Bearer " + token.access_token;
if (context.Request.Headers.ContainsKey("Authorization"))
{
context.Request.Headers["Authorization"] = headerValue;
}else
{
context.Request.Headers.Append("Authorization", headerValue);
}
}
}
await _next.Invoke(context);
}
else
{
// this is a logout request, clear the cookie by making it expire now
context.Response.Cookies.Append(authenticationCookieName,
"",
new Microsoft.AspNetCore.Http.CookieOptions()
{
Path = "/",
HttpOnly = true,
Secure = false,
Expires = DateTime.UtcNow.AddHours(-1)
});
context.Response.Redirect("/");
return;
}
}
else
{
await _next.Invoke(context);
}
}
}
In Configure() of startup.cs:
// use the AuthorizationHeader middleware
app.UseMiddleware<AuthorizationHeader>();
// Add a new middleware validating access tokens.
app.UseOAuthValidation();
You can then use the Authorize attribute normally.
[Authorize(Roles = "Administrator,User")]
This solution works for both api and mvc apps. For ajax and fetch requests however your must write some custom middleware that will not redirect the user to the login page and instead return a 401:
public class RedirectHandler
{
private readonly RequestDelegate _next;
public RedirectHandler(RequestDelegate next)
{
_next = next;
}
public bool IsAjaxRequest(HttpContext context)
{
return context.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
}
public bool IsFetchRequest(HttpContext context)
{
return context.Request.Headers["X-Requested-With"] == "Fetch";
}
public async Task Invoke(HttpContext context)
{
await _next.Invoke(context);
var ajax = IsAjaxRequest(context);
var fetch = IsFetchRequest(context);
if (context.Response.StatusCode == 302 && (ajax || fetch))
{
context.Response.Clear();
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("Unauthorized");
return;
}
}
}
I implemented the middleware successfully (based on Darxtar answer):
// TokenController.cs
public IActionResult Some()
{
...
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
Response.Cookies.Append(
"x",
tokenString,
new CookieOptions()
{
Path = "/"
}
);
return StatusCode(200, tokenString);
}
// JWTInHeaderMiddleware.cs
public class JWTInHeaderMiddleware
{
private readonly RequestDelegate _next;
public JWTInHeaderMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
var name = "x";
var cookie = context.Request.Cookies[name];
if (cookie != null)
if (!context.Request.Headers.ContainsKey("Authorization"))
context.Request.Headers.Append("Authorization", "Bearer " + cookie);
await _next.Invoke(context);
}
}
// Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseMiddleware<JWTInHeaderMiddleware>();
...
}
You can also use Events.OnMessageReceived property of JwtBearerOptions class
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddCookie()
.AddJwtBearer(options =>
{
options.Events = new()
{
OnMessageReceived = context =>
{
var request = context.HttpContext.Request;
var cookies = request.Cookies;
if (cookies.TryGetValue("AccessTokenCookieName",
out var accessTokenValue))
{
context.Token = accessTokenValue;
}
return Task.CompletedTask;
};
};
})
Related
I have a few API endpoints (.net core mvc) that will use the logged-in user (JWT) if the user is logged in, but anyone is allowed to call the method.
By using [AllowAnonymous] together with [Authorize] I get the functionality I'm looking for but if the sent JWT is expired, I'm not getting a 401, but instead it is treated as an anonymous request.
I need the authorization logic to treat the endpoint as it was [Authorize] only if there is a Authorization: Bearer header, meaning that if the token is expired, it should return a 401
This functionality is only needed on a few endpoints and not on a full controller
I've tried the combination of [AllowAnonymous] + [Authorize].
Also tried RequireAssertion when creating the policy, but doesn't seem to have been meant for this
The method I'm using for testing:
[HttpPost]
[Route("testToken")]
[AllowAnonymous]
[Authorize(Policy = AuthFilterConvension.POLICY_AUTHORIZE_WHEN_HAS_BEARER)]
public async Task<IActionResult> testToken()
{
var user = await _signInManager.UserManager.GetUserAsync(HttpContext.User);
return Ok(new {result = user});
}
Setting up auth to support both cookies + JWT:
services.AddAuthorization(o =>
{
o.AddPolicy(AuthFilterConvension.POLICY_AUTHORIZE_WHEN_HAS_BEARER, b =>
{
b.RequireRole("Admin");
b.RequireAuthenticatedUser();
b.AuthenticationSchemes = new List<string> {JwtBearerDefaults.AuthenticationScheme};
});
});
services.AddAuthentication()
.AddCookie()
.AddJwtBearer(cfg =>
{
var issuer = Environment.GetEnvironmentVariable("JWT_ISSUER");
var tokenKey = Environment.GetEnvironmentVariable("JWT_TOKEN_KEY");
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters
{
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidateAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidIssuer = issuer,
ValidAudience = issuer,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenKey))
};
}
);
I would expect the [Authorize] header to return 401 Unauthorized when using expired tokens instead of calling the method as anonymous. Is this possible to set up?
Update
I created the attribute like this:
public class AuthorizeBearerWhenPresent : ActionFilterAttribute, IAsyncAuthorizationFilter
{
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var headers = context.HttpContext.Request;
//TODO: Test header
var httpContext = context.HttpContext;
var authService = httpContext.RequestServices.GetRequiredService<IAuthorizationService>();
var authResult = await authService.AuthorizeAsync(httpContext.User, context,
AuthFilterConvension.POLICY_AUTHORIZE_WHEN_HAS_BEARER);
if (!authResult.Succeeded)
{
context.Result = new UnauthorizedResult();
}
}
}
However, no matter what I'm sending to the authService it returns true. Doesn't matter if I send an invalid JWT header or no header. Shouldn't this be the correct way to get executed a policy?
Thank you for your suggestions. While I was implementing the solution posted by Bart van der Drift, I stumbled upon the combination of a custom header together with using the IAuthorizationPolicyProvider
This way I'm using a custom policy name which I then overrides:
The constants in AuthFilterConvension:
public const string POLICY_JWT = "jwtPolicy";
public const string POLICY_AUTHORIZE_WHEN_HAS_BEARER = "authorizeWhenHasBearer";
Setting up the policies:
services.AddAuthorization(o =>
{
o.AddPolicy(AuthFilterConvension.POLICY_JWT, b =>
{
b.RequireRole("Admin");
b.RequireAuthenticatedUser();
b.AuthenticationSchemes = new List<string> {JwtBearerDefaults.AuthenticationScheme};
});
});
Add custom attribute:
public class AuthorizeBearerWhenPresent : AuthorizeAttribute
{
public AuthorizeBearerWhenPresent()
{
Policy = AuthFilterConvension.POLICY_AUTHORIZE_WHEN_HAS_BEARER;
}
}
The name POLICY_AUTHORIZE_WHEN_HAS_BEARER is not configured, but only used as a key in my CustomPolicyProvicer:
public class CustomPolicyProvider : IAuthorizationPolicyProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly DefaultAuthorizationPolicyProvider _fallbackPolicyProvider;
public CustomPolicyProvider(IHttpContextAccessor httpContextAccessor, IOptions<AuthorizationOptions> options)
{
_httpContextAccessor = httpContextAccessor;
_fallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}
public async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (AuthFilterConvension.POLICY_AUTHORIZE_WHEN_HAS_BEARER.Equals(policyName))
{
if (_httpContextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization"))
{
return await _fallbackPolicyProvider.GetPolicyAsync(AuthFilterConvension.POLICY_JWT);
}
return new AuthorizationPolicyBuilder()
.RequireAssertion(x=>true)
.Build();
}
return await _fallbackPolicyProvider.GetPolicyAsync(policyName);
}
public async Task<AuthorizationPolicy> GetDefaultPolicyAsync()
{
return await _fallbackPolicyProvider.GetDefaultPolicyAsync();
}
}
This way I can avoid doing custom handling of the JWT tokens
The following:
return new AuthorizationPolicyBuilder()
.RequireAssertion(x=>true)
.Build();
Is only used as a dummy "allow all"
You can create your own authentication logic by implementing the IAuthenticationFilter interface and inherit from ActionFilterAttribute:
public class MyCustomAuthentication : ActionFilterAttribute, System.Web.Http.Filters.IAuthenticationFilter
{
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// Handle the authorization header
}
}
Then in your controller, you can add the attribute either to the class or to specific methods.
[MyCustomAuthentication]
public async Task<IHttpActionResult> DoSomethingAsync()
{
// ...
}
If the token is present, you will also have to manually validate the token.
The code below is based on .Net Framework, so not sure if this also works for Core.
// Build URL based on your AAD-TenantId
var stsDiscoveryEndpoint = String.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", "<Your_tenant_ID>");
// Get tenant information that's used to validate incoming jwt tokens
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint);
// Get Config from AAD:
var config = await configManager.GetConfigurationAsync();
// Validate token:
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudience = "<Client_ID>",
ValidIssuer = "<Issuer>",
IssuerSigningTokens = config.SigningTokens,
CertificateValidator = X509CertificateValidator.ChainTrust
};
var parsedToken = (System.IdentityModel.Tokens.SecurityToken)new JwtSecurityToken();
try
{
tokenHandler.ValidateToken(token, validationParameters, out parsedToken);
result.ValidatedToken = (JwtSecurityToken)parsedToken;
}
catch (System.IdentityModel.Tokens.SecurityTokenValidationException stve)
{
// Handle error using stve.Message
}
Some more info and examples here.
You Could write a custom middleware to do so for example:
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext httpContext, IConfiguration configuration)
{
if (httpContext.Request.Headers.ContainsKey("Authorization"))
{
var authorizationToken = httpContext.Request.Headers["Authorization"].ToString();
if (!authorizationToken.StartsWith("bearer", StringComparison.OrdinalIgnoreCase))
{
await UnauthorizedResponseAsync(httpContext);
}
else
{
var token =authorizationToken.Substring("Bearer".Length).Trim())
if (httpContext.Request.Path == "Some of your path")
{
// DO your stuff
await _next.Invoke(httpContext);
}
}
}
else
{
await UnauthorizedResponseAsync(httpContext);
}
}
private static async Task UnauthorizedResponseAsync(HttpContext httpContext)
{
httpContext.Response.StatusCode = 401;
await httpContext.Response.WriteAsync("Unauthorized");
return;
}
I'm currently working on an OWIN OAuth implementation which uses JWT and supports token refreshing. I'm having intermittent problems with the token refresh process. The process works reliably on my development environment, but when published onto our Azure Service Fabric test environment, which is setup in a 3-node load-balanced configuration, the refresh token request often fails (not always!), and I get the infamous "invalid_grant" error.
I've found that the refresh token works successfully when being handled by the same service fabric node that issued it originally. However, it always fails when handled by a different node.
My understanding is that by using JWT, having a micro-service infrastructure deliver a load-balanced authentication server get's around the "machine-key" related issues that arise from using the OOTB access token format provided by OWIN.
Failed refresh tokens are making their way into the IAuthenticationTokenProvider.ReceiveAsync method, but the OAuthAuthorizationServerProvider.GrantRefreshToken method is never being hit, suggesting something in the OWIN middle-ware is not happy with the refresh token. Can anyone offer any insight into what the cause may be?
Now for the code, there's quite a bit - apologies for all the reading!
The authentication server is a service fabric stateless service, here's the ConfigureApp method:
protected override void ConfigureApp(IAppBuilder appBuilder)
{
appBuilder.UseCors(CorsOptions.AllowAll);
var oAuthAuthorizationServerOptions = InjectionContainer.GetInstance<OAuthAuthorizationServerOptions>();
appBuilder.UseOAuthAuthorizationServer(oAuthAuthorizationServerOptions);
appBuilder.UseJwtBearerAuthentication(InjectionContainer.GetInstance<JwtBearerAuthenticationOptions>());
appBuilder.UseWebApi(GetHttpConfiguration(InjectionContainer));
}
Here's the implementation of OAuthAuthorizationServerOptions:
public class AppOAuthOptions : OAuthAuthorizationServerOptions
{
public AppOAuthOptions(IAppJwtConfiguration configuration,
IAuthenticationTokenProvider authenticationTokenProvider,
IOAuthAuthorizationServerProvider authAuthorizationServerProvider)
{
AllowInsecureHttp = true;
TokenEndpointPath = "/token";
AccessTokenExpireTimeSpan = configuration.ExpirationMinutes;
AccessTokenFormat = new AppJwtWriterFormat(this, configuration);
Provider = authAuthorizationServerProvider;
RefreshTokenProvider = authenticationTokenProvider;
}
}
And here's the JwtBearerAuthenticationOptions implementation:
public class AppJwtOptions : JwtBearerAuthenticationOptions
{
public AppJwtOptions(IAppJwtConfiguration config)
{
AuthenticationMode = AuthenticationMode.Active;
AllowedAudiences = new[] {config.JwtAudience};
IssuerSecurityTokenProviders = new[]
{
new SymmetricKeyIssuerSecurityTokenProvider(
config.JwtIssuer,
Convert.ToBase64String(Encoding.UTF8.GetBytes(config.JwtKey)))
};
}
}
public class InMemoryJwtConfiguration : IAppJwtConfiguration
{
AppSettings _appSettings;
public InMemoryJwtConfiguration(AppSettings appSettings)
{
_appSettings = appSettings;
}
public int ExpirationMinutes
{
get { return 15; }
set { }
}
public string JwtAudience
{
get { return "CENSORED AUDIENCE"; }
set { }
}
public string JwtIssuer
{
get { return "CENSORED ISSUER"; }
set { }
}
public string JwtKey
{
get { return "CENSORED KEY :)"; }
set { }
}
public int RefreshTokenExpirationMinutes
{
get { return 60; }
set { }
}
public string TokenPath
{
get { return "/token"; }
set { }
}
}
And the ISecureData implementation:
public class AppJwtWriterFormat : ISecureDataFormat<AuthenticationTicket>
{
public AppJwtWriterFormat(
OAuthAuthorizationServerOptions options,
IAppJwtConfiguration configuration)
{
_options = options;
_configuration = configuration;
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
var now = DateTime.UtcNow;
var expires = now.AddMinutes(_options.AccessTokenExpireTimeSpan.TotalMinutes);
var symmetricKey = Encoding.UTF8.GetBytes(_configuration.JwtKey);
var signingCredentials = new SigningCredentials(
new InMemorySymmetricSecurityKey(symmetricKey),
SignatureAlgorithm, DigestAlgorithm);
var token = new JwtSecurityToken(
_configuration.JwtIssuer,
_configuration.JwtAudience,
data.Identity.Claims,
now,
expires,
signingCredentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public AuthenticationTicket Unprotect(string protectedText)
{
throw new NotImplementedException();
}
}
This is the IAuthenticationTokenProvider implementation:
public class RefreshTokenProvider : IAuthenticationTokenProvider
{
private readonly IAppJwtConfiguration _configuration;
private readonly IContainer _container;
public RefreshTokenProvider(IAppJwtConfiguration configuration, IContainer container)
{
_configuration = configuration;
_container = container;
_telemetry = telemetry;
}
public void Create(AuthenticationTokenCreateContext context)
{
CreateAsync(context).Wait();
}
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
try
{
var refreshTokenId = Guid.NewGuid().ToString("n");
var now = DateTime.UtcNow;
using (var container = _container.GetNestedContainer())
{
var hashLogic = container.GetInstance<IHashLogic>();
var tokenStoreLogic = container.GetInstance<ITokenStoreLogic>();
var userName = context.Ticket.Identity.FindFirst(ClaimTypes.UserData).Value;
var userToken = new UserToken
{
Email = userName,
RefreshTokenIdHash = hashLogic.HashInput(refreshTokenId),
Subject = context.Ticket.Identity.Name,
RefreshTokenExpiresUtc =
now.AddMinutes(Convert.ToDouble(_configuration.RefreshTokenExpirationMinutes)),
AccessTokenExpirationDateTime =
now.AddMinutes(Convert.ToDouble(_configuration.ExpirationMinutes))
};
context.Ticket.Properties.IssuedUtc = now;
context.Ticket.Properties.ExpiresUtc = userToken.RefreshTokenExpiresUtc;
context.Ticket.Properties.AllowRefresh = true;
userToken.RefreshToken = context.SerializeTicket();
await tokenStoreLogic.CreateUserTokenAsync(userToken);
context.SetToken(refreshTokenId);
}
}
catch (Exception ex)
{
// exception logging removed for brevity
throw;
}
}
public void Receive(AuthenticationTokenReceiveContext context)
{
ReceiveAsync(context).Wait();
}
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
try
{
using (var container = _container.GetNestedContainer())
{
var hashLogic = container.GetInstance<IHashLogic>();
var tokenStoreLogic = container.GetInstance<ITokenStoreLogic>();
var hashedTokenId = hashLogic.HashInput(context.Token);
var refreshToken = await tokenStoreLogic.FindRefreshTokenAsync(hashedTokenId);
if (refreshToken == null)
{
return;
}
context.DeserializeTicket(refreshToken.RefreshToken);
await tokenStoreLogic.DeleteRefreshTokenAsync(hashedTokenId);
}
}
catch (Exception ex)
{
// exception logging removed for brevity
throw;
}
}
}
And finally, this is the OAuthAuthorizationServerProvider implementation:
public class AppOAuthProvider : OAuthAuthorizationServerProvider
{
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
if (context.ClientId != null)
{
context.Rejected();
return Task.FromResult(0);
}
// Change authentication ticket for refresh token requests
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
newIdentity.AddClaim(new Claim("newClaim", "refreshToken"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult(0);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
using (var container = _container.GetNestedContainer())
{
var requestedAuthenticationType = context.Request.Query["type"];
var requiredAuthenticationType = (int)AuthenticationType.None;
if (string.IsNullOrEmpty(requestedAuthenticationType) || !int.TryParse(requestedAuthenticationType, out requiredAuthenticationType))
{
context.SetError("Authentication Type Missing", "Type parameter is required to check which type of user you are trying to authenticate with.");
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
return;
}
var authenticationWorker = GetInstance<IAuthenticationWorker>(container);
var result = await authenticationWorker.AuthenticateAsync(new AuthenticationRequestViewModel
{
UserName = context.UserName,
Password = context.Password,
IpAddress = context.Request.RemoteIpAddress ?? "",
UserAgent = context.Request.Headers.ContainsKey("User-Agent") ? context.Request.Headers["User-Agent"] : ""
});
if (result.SignInStatus != SignInStatus.Success)
{
context.SetError(result.SignInStatus.ToString(), result.Message);
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
// After we have successfully logged in. Check the authentication type for the just authenticated user
var userAuthenticationType = (int)result.AuthenticatedUserViewModel.Type;
// Check if the auth types match
if (userAuthenticationType != requiredAuthenticationType)
{
context.SetError("Invalid Account", "InvalidAccountForPortal");
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return;
}
var identity = SetClaimsIdentity(context, result.AuthenticatedUserViewModel);
context.Validated(identity);
}
}
public override async Task TokenEndpointResponse(OAuthTokenEndpointResponseContext context)
{
using (var container = GetNestedContainer())
{
var email = context.Identity.FindFirst(ClaimTypes.UserData).Value;
var accessTokenHash = _hashLogic.HashInput(context.AccessToken);
var tokenStoreLogic = GetInstance<ITokenStoreLogic>(container);
await tokenStoreLogic.UpdateUserTokenAsync(email, accessTokenHash);
var authLogic = GetInstance<IAuthenticationLogic>(container);
var userDetail = await authLogic.GetDetailsAsync(email);
context.AdditionalResponseParameters.Add("user_id", email);
context.AdditionalResponseParameters.Add("user_name", userDetail.Name);
context.AdditionalResponseParameters.Add("user_known_as", userDetail.KnownAs);
context.AdditionalResponseParameters.Add("authentication_type", userDetail.Type);
}
await base.TokenEndpointResponse(context);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
return Task.FromResult(0);
}
private ClaimsIdentity SetClaimsIdentity(OAuthGrantResourceOwnerCredentialsContext context, AuthenticatedUserViewModel user)
{
var identity = new ClaimsIdentity(
new[]
{
new Claim(ClaimTypes.Name, context.UserName),
new Claim(ClaimTypes.SerialNumber, user.SerialNumber),
new Claim(ClaimTypes.UserData, user.Email.ToString(CultureInfo.InvariantCulture)),
new Claim(ClaimTypeUrls.AdminScope, user.Scope.ToString()),
new Claim(ClaimTypeUrls.DriverId, user.DriverId.ToString(CultureInfo.InvariantCulture)),
new Claim(ClaimTypeUrls.AdministratorId, user.AdministratorId.ToString(CultureInfo.InvariantCulture))
},
_authenticationType
);
//add roles
var roles = user.Roles;
foreach (var role in roles)
identity.AddClaim(new Claim(ClaimTypes.Role, role));
return identity;
}
}
I am trying to figure out how to get the claim out of my token.
I will try an keep the explanation short
I have an HTML page that does a post to my web api, does and auth
check and returns an JWT token
when i get the token back i want to send it to different url, and the way i am doing it is using a querystring. I know i can use cookies but for this app we dont want to use them. So if my url looks like this http://somedomain/checkout/?token=bearer token comes here
I am using Owin middleware and this is what i have so far
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new ApplicationOAuthBearerAuthenticationProvider(),
});
public class ApplicationOAuthBearerAuthenticationProvider
: OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var token = HttpContext.Current.Request.QueryString["token"];
if (!string.IsNullOrEmpty(token))
context.Token = token;
return Task.FromResult<object>(null);
}
}
But how do i get the Claims out of the Token or just check the IsAuthenticated
I tried the Following inside my controller just to check, but the IsAuthenticated is always false
var identity = (ClaimsIdentity) HttpContext.Current.GetOwinContext().Authentication.User.Identity;
if (!identity.IsAuthenticated)
return;
var id = identity.FindFirst(ClaimTypes.NameIdentifier);
OK so I managed to figure it out. The above code that I had is all working well but I needed to add the UseJwtBearerAuthentication middle ware.
One thing I did end up changing from my original code was i changed the context.Token = token; to context.Request.Headers.Add("Authorization", new[] { string.Format("Bearer {0}", token) });
So my startup class looks like this...
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
Provider = new ApplicationOAuthBearerAuthenticationProvider(),
});
app.UseJwtBearerAuthentication(JwtOptions());
ConfigureAuth(app);
}
private static JwtBearerAuthenticationOptions JwtOptions()
{
var key = Encoding.UTF8.GetBytes(ConfigurationManager.AppSettings["auth:key"]);
var jwt = new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters
{
ValidAudience = Some Audience,
ValidIssuer = Some Issuer,
IssuerSigningToken = new BinarySecretSecurityToken(key),
RequireExpirationTime = false,
ValidateLifetime = false
}
};
return jwt;
}
public class ApplicationOAuthBearerAuthenticationProvider
: OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
if (context == null)
throw new ArgumentNullException("context");
var token = HttpContext.Current.Request.QueryString["token"];
if (!string.IsNullOrEmpty(token))
context.Request.Headers.Add("Authorization", new[] { string.Format("Bearer {0}", token) });
return Task.FromResult<object>(null);
}
}
}
I'm using ExchangeUserCredentialForToken function to get the token from the Authorization server. It's working fine when my user exists in my databas, but when the credentials are incorect I would like to send back a message to the client. I'm using the following 2 lines of code to set the error message:
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Rejected();
But on the client side I'm getting only protocol error (error 400). Can you help me how can I get the error message set on the server side on the authorization server?
The full app config from the Authorization server:
using Constants;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Infrastructure;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using AuthorizationServer.Entities;
using AuthorizationServer.Entities.Infrastructure.Abstract;
using AuthorizationServer.Entities.Infrastructure.Concrete;
namespace AuthorizationServer
{
public partial class Startup
{
private IEmployeeRepository Repository;
public void ConfigureAuth(IAppBuilder app)
{
//instanciate the repository
Repository = new EmployeeRepository();
// Enable Application Sign In Cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Application",
AuthenticationMode = AuthenticationMode.Passive,
LoginPath = new PathString(Paths.LoginPath),
LogoutPath = new PathString(Paths.LogoutPath),
});
// Enable External Sign In Cookie
app.SetDefaultSignInAsAuthenticationType("External");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "External",
AuthenticationMode = AuthenticationMode.Passive,
CookieName = CookieAuthenticationDefaults.CookiePrefix + "External",
ExpireTimeSpan = TimeSpan.FromMinutes(5),
});
// Enable google authentication
app.UseGoogleAuthentication();
// Setup Authorization Server
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
AuthorizeEndpointPath = new PathString(Paths.AuthorizePath),
TokenEndpointPath = new PathString(Paths.TokenPath),
ApplicationCanDisplayErrors = true,
#if DEBUG
AllowInsecureHttp = true,
#endif
// Authorization server provider which controls the lifecycle of Authorization Server
Provider = new OAuthAuthorizationServerProvider
{
OnValidateClientRedirectUri = ValidateClientRedirectUri,
OnValidateClientAuthentication = ValidateClientAuthentication,
OnGrantResourceOwnerCredentials = GrantResourceOwnerCredentials,
OnGrantClientCredentials = GrantClientCredetails
},
// Authorization code provider which creates and receives authorization code
AuthorizationCodeProvider = new AuthenticationTokenProvider
{
OnCreate = CreateAuthenticationCode,
OnReceive = ReceiveAuthenticationCode,
},
// Refresh token provider which creates and receives referesh token
RefreshTokenProvider = new AuthenticationTokenProvider
{
OnCreate = CreateRefreshToken,
OnReceive = ReceiveRefreshToken,
}
});
// indicate our intent to use bearer authentication
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AuthenticationType = "Bearer",
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active
});
}
private Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == Clients.Client1.Id)
{
context.Validated(Clients.Client1.RedirectUrl);
}
else if (context.ClientId == Clients.Client2.Id)
{
context.Validated(Clients.Client2.RedirectUrl);
}
return Task.FromResult(0);
}
private Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientname;
string clientpassword;
if (context.TryGetBasicCredentials(out clientname, out clientpassword) ||
context.TryGetFormCredentials(out clientname, out clientpassword))
{
employee Employee = Repository.GetEmployee(clientname, clientpassword);
if (Employee != null)
{
context.Validated();
}
else
{
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Rejected();
}
}
return Task.FromResult(0);
}
private Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.UserName, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private Task GrantClientCredetails(OAuthGrantClientCredentialsContext context)
{
var identity = new ClaimsIdentity(new GenericIdentity(context.ClientId, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
context.Validated(identity);
return Task.FromResult(0);
}
private readonly ConcurrentDictionary<string, string> _authenticationCodes =
new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)
{
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
_authenticationCodes[context.Token] = context.SerializeTicket();
}
private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
{
string value;
if (_authenticationCodes.TryRemove(context.Token, out value))
{
context.DeserializeTicket(value);
}
}
private void CreateRefreshToken(AuthenticationTokenCreateContext context)
{
context.SetToken(context.SerializeTicket());
}
private void ReceiveRefreshToken(AuthenticationTokenReceiveContext context)
{
context.DeserializeTicket(context.Token);
}
}
}
After hours of searching the web and reading blobs, and the owin documentation, I have found a way to return a 401 for a failed login attempt.
I realize adding the header below is a bit of a hack, but I could not find any way to read the IOwinContext.Response.Body stream to look for the error message.
First of all, In the OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials I used SetError() and added a Headers to the response
context.SetError("Autorization Error", "The username or password is incorrect!");
context.Response.Headers.Add("AuthorizationResponse", new[] { "Failed" });
Now, you have a way to differentiate between a 400 error for a failed athentication request, and a 400 error caused by something else.
The next step is to create a class that inherits OwinMiddleware. This class checks the outgoing response and if the StatusCode == 400 and the Header above is present, it changes the StatucCode to 401.
public class InvalidAuthenticationMiddleware : OwinMiddleware
{
public InvalidAuthenticationMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
await Next.Invoke(context);
if (context.Response.StatusCode == 400 && context.Response.Headers.ContainsKey("AuthorizationResponse"))
{
context.Response.Headers.Remove("AuthorizationResponse");
context.Response.StatusCode = 401;
}
}
}
The last thing to do is in your Startup.Configuration method, register the class you just created. I registered it before I did anything else in the method.
app.Use<InvalidAuthenticationMiddleware>();
Here is a full solution, using Jeff's concepts in conjunction with my original post.
1) Setting the error message in the context
If you call context.Rejected() after you have set the error message, then the error message is removed (see example below):
context.SetError("Account locked",
"You have exceeded the total allowed failed logins. Please try back in an hour.");
context.Rejected();
You will want to remove the context.Rejected() from your Task. Please note the definitions of the Rejected and SetError methods are:
Rejected:
Marks this context as not validated by the application. IsValidated and HasError become false as a result of calling.
SetError:
Marks this context as not validated by the application and assigns various error information properties. HasError becomes true and IsValidated becomes false as a result of calling.
Again, by calling the Rejected method after you set the error, the context will be marked as not having an error and the error message will be removed.
2) Setting the status code of the response: Using Jeff's example, with a bit of a spin on it.
Instead of using a magic string, I would create a global property for setting the tag for the status code. In your static global class, create a property for flagging the status code (I used X-Challenge, but you of course could use whatever you choose.) This will be used to flag the header property that is added in the response.
public static class ServerGlobalVariables
{
//Your other properties...
public const string OwinChallengeFlag = "X-Challenge";
}
Then in the various tasks of your OAuthAuthorizationServerProvider, you will add the tag as the key to a new header value in the response. Using the HttpStatusCode enum in conjunction with you global flag, you will have access to all of the various status codes and you avoid a magic string.
//Set the error message
context.SetError("Account locked",
"You have exceeded the total allowed failed logins. Please try back in an hour.");
//Add your flag to the header of the response
context.Response.Headers.Add(ServerGlobalVariables.OwinChallengeFlag,
new[] { ((int)HttpStatusCode.Unauthorized).ToString() });
In the customer OwinMiddleware, you can search for the flag in the header using the global variable:
//This class handles all the OwinMiddleware responses, so the name should
//not just focus on invalid authentication
public class CustomAuthenticationMiddleware : OwinMiddleware
{
public CustomAuthenticationMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
await Next.Invoke(context);
if (context.Response.StatusCode == 400
&& context.Response.Headers.ContainsKey(
ServerGlobalVariables.OwinChallengeFlag))
{
var headerValues = context.Response.Headers.GetValues
(ServerGlobalVariables.OwinChallengeFlag);
context.Response.StatusCode =
Convert.ToInt16(headerValues.FirstOrDefault());
context.Response.Headers.Remove(
ServerGlobalVariables.OwinChallengeFlag);
}
}
}
Finally, as Jeff pointed out, you have to register this custom OwinMiddleware in your Startup.Configuration or Startup.ConfigureAuth method:
app.Use<CustomAuthenticationMiddleware>();
Using the above solution, you can now set the status codes and a custom error message, like the ones shown below:
Invalid user name or password
This account has exceeded the maximum number of attempts
The email account has not been confirmed
3) Extracting the error message from the ProtocolException
In the client application, a ProtocolException will need to be caught and processed. Something like this will give you the answer:
//Need to create a class to deserialize the Json
//Create this somewhere in your application
public class OAuthErrorMsg
{
public string error { get; set; }
public string error_description { get; set; }
public string error_uri { get; set; }
}
//Need to make sure to include Newtonsoft.Json
using Newtonsoft.Json;
//Code for your object....
private void login()
{
try
{
var state = _webServerClient.ExchangeUserCredentialForToken(
this.emailTextBox.Text,
this.passwordBox.Password.Trim(),
scopes: new string[] { "PublicProfile" });
_accessToken = state.AccessToken;
_refreshToken = state.RefreshToken;
}
catch (ProtocolException ex)
{
var webException = ex.InnerException as WebException;
OAuthErrorMsg error =
JsonConvert.DeserializeObject<OAuthErrorMsg>(
ExtractResponseString(webException));
var errorMessage = error.error_description;
//Now it's up to you how you process the errorMessage
}
}
public static string ExtractResponseString(WebException webException)
{
if (webException == null || webException.Response == null)
return null;
var responseStream =
webException.Response.GetResponseStream() as MemoryStream;
if (responseStream == null)
return null;
var responseBytes = responseStream.ToArray();
var responseString = Encoding.UTF8.GetString(responseBytes);
return responseString;
}
I have tested this and it works perfectly in VS2013 Pro with 4.5!!
(please note, I did not include all the necessary namespaces or the additional code since this will vary depending on the application: WPF, MVC, or Winform. Also, I didn't discuss error handling, so you will want to make sure to implement proper error handling throughout your solution.)
Jeff's solution does not work for me, but when I use OnSendingHeaders it works fine:
public class InvalidAuthenticationMiddleware : OwinMiddleware
{
public InvalidAuthenticationMiddleware(OwinMiddleware next) : base(next) { }
public override async Task Invoke(IOwinContext context)
{
context.Response.OnSendingHeaders(state =>
{
var response = (OwinResponse)state;
if (!response.Headers.ContainsKey("AuthorizationResponse") && response.StatusCode != 400) return;
response.Headers.Remove("AuthorizationResponse");
response.StatusCode = 401;
}, context.Response);
await Next.Invoke(context);
}
}
I'm trying to implement OAuth Bearer Authentication with Owin. When an invalid or expired token is passed, the default implementation is to log this as a warning and just don't set an Identity. I however would like to reject the whole request with an error in this case. But how would I do this?
After digging through the code I found out that in OAuthBearerAuthenticationHandler it will parse the token using a fallback mechanism when the provided AuthenticationTokenProvider did not parse any ticket (like the default implementation). This handler will log a warning when the token could not be parsed to any ticket or when it expired.
But I can't find any place to plug in my own logic to what happens when the token is invalid or expired. I could theoretically check this on my own in the AuthenticationTokenProvider, but then I would have to reimplement the logic (= copy it over) for creating and reading the token. Also this seems just out of place, as this class seems to be only responsible for creating and parsing tokens. I also don't see a way to plug in my own implementation of the OAuthBearerAuthenticationHandler in the OAuthBearerAuthenticationMiddleware.
Apparently my best and cleanest shot would be to reimplement the whole middleware, but this also seems very overkill.
What do I overlook? How would I go on about this the best?
edit:
For clarification. I know by not setting an identity the request will be rejected with 401 Unauthorized later in the Web API. But I personally see this as really bad style, silently swallowing an erroneous access token without any notification. This way you don't get to know that your token is crap, you just get to know you're not authorized.
I had a similar issue, i think the answer is to late but someone will come here with a similar problem:
I used this nuget package for validate authentication, but i think any method can help: https://www.nuget.org/packages/WebApi.AuthenticationFilter. You can read its documentation in this site https://github.com/mbenford/WebApi-AuthenticationFilter
AuthenticationFilter.cs
public class AuthenticationFilter : AuthenticationFilterAttribute{
public override void OnAuthentication(HttpAuthenticationContext context)
{
System.Net.Http.Formatting.MediaTypeFormatter jsonFormatter = new System.Net.Http.Formatting.JsonMediaTypeFormatter();
var ci = context.Principal.Identity as ClaimsIdentity;
//First of all we are going to check that the request has the required Authorization header. If not set the Error
var authHeader = context.Request.Headers.Authorization;
//Change "Bearer" for the needed schema
if (authHeader == null || authHeader.Scheme != "Bearer")
{
context.ErrorResult = context.ErrorResult = new AuthenticationFailureResult("unauthorized", context.Request,
new { Error = new { Code = 401, Message = "Request require authorization" } });
}
//If the token has expired the property "IsAuthenticated" would be False, then set the error
else if (!ci.IsAuthenticated)
{
context.ErrorResult = new AuthenticationFailureResult("unauthorized", context.Request,
new { Error = new { Code = 401, Message = "The Token has expired" } });
}
}}
AuthenticationFailureResult.cs
public class AuthenticationFailureResult : IHttpActionResult{
private object ResponseMessage;
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request, object responseMessage)
{
ReasonPhrase = reasonPhrase;
Request = request;
ResponseMessage = responseMessage;
}
public string ReasonPhrase { get; private set; }
public HttpRequestMessage Request { get; private set; }
public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
return Task.FromResult(Execute());
}
private HttpResponseMessage Execute()
{
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
System.Net.Http.Formatting.MediaTypeFormatter jsonFormatter = new System.Net.Http.Formatting.JsonMediaTypeFormatter();
response.Content = new System.Net.Http.ObjectContent<object>(ResponseMessage, jsonFormatter);
response.RequestMessage = Request;
response.ReasonPhrase = ReasonPhrase;
return response;
}}
Response examples:
{"Error":{"Code":401,"Message":"Request require authorization"}}
{"Error":{"Code":401,"Message":"The Token has expired"}}
Fonts and inspiration documentation:
//github.com/mbenford/WebApi-AuthenticationFilter
//www.asp.net/web-api/overview/security/authentication-filters
Yeah, I did not find 'good' solution for this,
I also don't see a way to plug in my own implementation of the
OAuthBearerAuthenticationHandler in the
OAuthBearerAuthenticationMiddleware.
Apparently my best and cleanest shot would be to reimplement the whole
middleware, but this also seems very overkill.
Agreed, but that's what I did (before reading your post). I copy & pasted three owin classes, and made it so that it sets property in Owins context, which can be later checked by other handlers.
public static class OAuthBearerAuthenticationExtensions
{
public static IAppBuilder UseOAuthBearerAuthenticationExtended(this IAppBuilder app, OAuthBearerAuthenticationOptions options)
{
if (app == null)
throw new ArgumentNullException(nameof(app));
app.Use(typeof(OAuthBearerAuthenticationMiddlewareExtended), app, options);
app.UseStageMarker(PipelineStage.Authenticate);
return app;
}
}
internal class OAuthBearerAuthenticationHandlerExtended : AuthenticationHandler<OAuthBearerAuthenticationOptions>
{
private readonly ILogger _logger;
private readonly string _challenge;
public OAuthBearerAuthenticationHandlerExtended(ILogger logger, string challenge)
{
_logger = logger;
_challenge = challenge;
}
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
try
{
// Find token in default location
string requestToken = null;
string authorization = Request.Headers.Get("Authorization");
if (!string.IsNullOrEmpty(authorization))
{
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
requestToken = authorization.Substring("Bearer ".Length).Trim();
}
}
// Give application opportunity to find from a different location, adjust, or reject token
var requestTokenContext = new OAuthRequestTokenContext(Context, requestToken);
await Options.Provider.RequestToken(requestTokenContext);
// If no token found, no further work possible
if (string.IsNullOrEmpty(requestTokenContext.Token))
{
return null;
}
// Call provider to process the token into data
var tokenReceiveContext = new AuthenticationTokenReceiveContext(
Context,
Options.AccessTokenFormat,
requestTokenContext.Token);
await Options.AccessTokenProvider.ReceiveAsync(tokenReceiveContext);
if (tokenReceiveContext.Ticket == null)
{
tokenReceiveContext.DeserializeTicket(tokenReceiveContext.Token);
}
AuthenticationTicket ticket = tokenReceiveContext.Ticket;
if (ticket == null)
{
_logger.WriteWarning("invalid bearer token received");
Context.Set("oauth.token_invalid", true);
return null;
}
// Validate expiration time if present
DateTimeOffset currentUtc = Options.SystemClock.UtcNow;
if (ticket.Properties.ExpiresUtc.HasValue &&
ticket.Properties.ExpiresUtc.Value < currentUtc)
{
_logger.WriteWarning("expired bearer token received");
Context.Set("oauth.token_expired", true);
return null;
}
// Give application final opportunity to override results
var context = new OAuthValidateIdentityContext(Context, Options, ticket);
if (ticket != null &&
ticket.Identity != null &&
ticket.Identity.IsAuthenticated)
{
// bearer token with identity starts validated
context.Validated();
}
if (Options.Provider != null)
{
await Options.Provider.ValidateIdentity(context);
}
if (!context.IsValidated)
{
return null;
}
// resulting identity values go back to caller
return context.Ticket;
}
catch (Exception ex)
{
_logger.WriteError("Authentication failed", ex);
return null;
}
}
protected override Task ApplyResponseChallengeAsync()
{
if (Response.StatusCode != 401)
{
return Task.FromResult<object>(null);
}
AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
if (challenge != null)
{
OAuthChallengeContext challengeContext = new OAuthChallengeContext(Context, _challenge);
Options.Provider.ApplyChallenge(challengeContext);
}
return Task.FromResult<object>(null);
}
}
public class OAuthBearerAuthenticationMiddlewareExtended : AuthenticationMiddleware<OAuthBearerAuthenticationOptions>
{
private readonly ILogger _logger;
private readonly string _challenge;
/// <summary>
/// Bearer authentication component which is added to an OWIN pipeline. This constructor is not
/// called by application code directly, instead it is added by calling the the IAppBuilder UseOAuthBearerAuthentication
/// extension method.
///
/// </summary>
public OAuthBearerAuthenticationMiddlewareExtended(OwinMiddleware next, IAppBuilder app, OAuthBearerAuthenticationOptions options)
: base(next, options)
{
_logger = AppBuilderLoggerExtensions.CreateLogger<OAuthBearerAuthenticationMiddlewareExtended>(app);
_challenge = string.IsNullOrWhiteSpace(Options.Challenge) ? (!string.IsNullOrWhiteSpace(Options.Realm) ? "Bearer realm=\"" + this.Options.Realm + "\"" : "Bearer") : this.Options.Challenge;
if (Options.Provider == null)
Options.Provider = new OAuthBearerAuthenticationProvider();
if (Options.AccessTokenFormat == null)
Options.AccessTokenFormat = new TicketDataFormat(
Microsoft.Owin.Security.DataProtection.AppBuilderExtensions.CreateDataProtector(app, typeof(OAuthBearerAuthenticationMiddleware).Namespace, "Access_Token", "v1"));
if (Options.AccessTokenProvider != null)
return;
Options.AccessTokenProvider = new AuthenticationTokenProvider();
}
/// <summary>
/// Called by the AuthenticationMiddleware base class to create a per-request handler.
///
/// </summary>
///
/// <returns>
/// A new instance of the request handler
/// </returns>
protected override AuthenticationHandler<OAuthBearerAuthenticationOptions> CreateHandler()
{
return new OAuthBearerAuthenticationHandlerExtended(_logger, _challenge);
}
}
Then I wrote my own authorization filter, which will be applied globally:
public class AuthorizeAttributeExtended : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
{
var tokenHasExpired = false;
var owinContext = OwinHttpRequestMessageExtensions.GetOwinContext(actionContext.Request);
if (owinContext != null)
{
tokenHasExpired = owinContext.Environment.ContainsKey("oauth.token_expired");
}
if (tokenHasExpired)
{
actionContext.Response = new AuthenticationFailureMessage("unauthorized", actionContext.Request,
new
{
error = "invalid_token",
error_message = "The Token has expired"
});
}
else
{
actionContext.Response = new AuthenticationFailureMessage("unauthorized", actionContext.Request,
new
{
error = "invalid_request",
error_message = "The Token is invalid"
});
}
}
}
public class AuthenticationFailureMessage : HttpResponseMessage
{
public AuthenticationFailureMessage(string reasonPhrase, HttpRequestMessage request, object responseMessage)
: base(HttpStatusCode.Unauthorized)
{
MediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
Content = new ObjectContent<object>(responseMessage, jsonFormatter);
RequestMessage = request;
ReasonPhrase = reasonPhrase;
}
}
my WebApiConfig:
config.Filters.Add(new AuthorizeAttributeExtended());
How my configureOAuth looks like:
public void ConfigureOAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
OAuthBearerOptions = new OAuthBearerAuthenticationOptions()
{
};
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10),
Provider = new SimpleAuthorizationServerProvider(),
RefreshTokenProvider = new SimpleRefreshTokenProvider(),
AuthenticationMode = AuthenticationMode.Active
};
FacebookAuthOptions = new CustomFacebookAuthenticationOptions();
app.UseFacebookAuthentication(FacebookAuthOptions);
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthenticationExtended(OAuthBearerOptions);
}
I will try & get this to main branch of oAuth middleware, it seems like an obvious use case, unless I am missing something.
I came across this problem recently. We wanted to return a JSON message if the user's access token had expired, allowing the consumer web application to silently refresh the access token and re-issue the API request. We also didn't want to rely on the exceptions thrown for token lifetime validation.
Not wanting to re-implement any middleware, we specified the Provider option inside JwtBearerAuthenticationOptions and added a delegate to handle the OnRequestTokenMethod. The delegate checks to see if it can read the token passed to the middleware and sets a boolean inside the OWIN context if it's expired.
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = tokenValidationParameters,
Provider = new OAuthBearerAuthenticationProvider
{
OnRequestToken = (ctx) =>
{
if (!string.IsNullOrEmpty(ctx.Token))
{
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
if (handler.CanReadToken(ctx.Token))
{
JwtSecurityToken jwtToken = handler.ReadJwtToken(ctx.Token);
if (jwtToken.IsExpired())
ctx.OwinContext.Set<bool>("expiredToken", true);
}
}
return Task.CompletedTask;
}
}
});
For convenience I added a quick extension method to check if a JWT expired:
public static class JwtSecurityTokenExtensions
{
public static bool IsExpired (this JwtSecurityToken token)
{
if (DateTime.UtcNow > token.ValidTo.ToUniversalTime())
return true;
return false;
}
}
We ended up using a middleware to check on the state of that boolean:
app.Use((context, next) =>
{
bool expiredToken = context.Get<bool>("expiredToken");
if (expiredToken)
{
// do stuff
}
return next.Invoke();
});
app.UseStageMarker(PipelineStage.Authenticate);
Not exactly the most efficient code, since we're parsing the token again after the middleware already did and also introducing a new middleware to act on the result of the check, but it's a fresh perspective nonetheless.
If authentication fails (meaning the token is expired) then that layer doesn't set the user, as you said. It's up the the authorization layer (later on) to reject the call. So for your scenario your Web API would need to deny access to an anonymous caller. Use the [Authorize] authorization filter attribute.