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;
}
Related
I have created ASP.NET Core WebApi protected with IdentityServer4 using ROPC flow (using this example: https://github.com/robisim74/AngularSPAWebAPI).
How to manually generate access_token from the server without password?
[HttpPost("loginas/{id}")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> LoginAs(int id, [FromServices] ITokenService TS,
[FromServices] IUserClaimsPrincipalFactory<ApplicationUser> principalFactory,
[FromServices] IdentityServerOptions options)
{
var Request = new TokenCreationRequest();
var User = await userManager.FindByIdAsync(id.ToString());
var IdentityPricipal = await principalFactory.CreateAsync(User);
var IdServerPrincipal = IdentityServerPrincipal.Create(User.Id.ToString(), User.UserName, IdentityPricipal.Claims.ToArray());
Request.Subject = IdServerPrincipal;
Request.IncludeAllIdentityClaims = true;
Request.ValidatedRequest = new ValidatedRequest();
Request.ValidatedRequest.Subject = Request.Subject;
Request.ValidatedRequest.SetClient(Config.GetClients().First());
Request.Resources = new Resources(Config.GetIdentityResources(), Config.GetApiResources());
Request.ValidatedRequest.Options = options;
Request.ValidatedRequest.ClientClaims = IdServerPrincipal.Claims.ToArray();
var Token = await TS.CreateAccessTokenAsync(Request);
Token.Issuer = "http://" + HttpContext.Request.Host.Value;
var TokenValue = await TS.CreateSecurityTokenAsync(Token);
return Ok(TokenValue);
}
For a newly released IdentityServer 2.0.0 the code needs some modifications:
[HttpPost("loginas/{id}")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> LoginAs(int id, [FromServices] ITokenService TS,
[FromServices] IUserClaimsPrincipalFactory<ApplicationUser> principalFactory,
[FromServices] IdentityServerOptions options)
{
var Request = new TokenCreationRequest();
var User = await userManager.FindByIdAsync(id.ToString());
var IdentityPricipal = await principalFactory.CreateAsync(User);
var IdentityUser = new IdentityServerUser(User.Id.ToString());
IdentityUser.AdditionalClaims = IdentityPricipal.Claims.ToArray();
IdentityUser.DisplayName = User.UserName;
IdentityUser.AuthenticationTime = System.DateTime.UtcNow;
IdentityUser.IdentityProvider = IdentityServerConstants.LocalIdentityProvider;
Request.Subject = IdentityUser.CreatePrincipal();
Request.IncludeAllIdentityClaims = true;
Request.ValidatedRequest = new ValidatedRequest();
Request.ValidatedRequest.Subject = Request.Subject;
Request.ValidatedRequest.SetClient(Config.GetClients().First());
Request.Resources = new Resources(Config.GetIdentityResources(), Config.GetApiResources());
Request.ValidatedRequest.Options = options;
Request.ValidatedRequest.ClientClaims = IdentityUser.AdditionalClaims;
var Token = await TS.CreateAccessTokenAsync(Request);
Token.Issuer = HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value;
var TokenValue = await TS.CreateSecurityTokenAsync(Token);
return Ok(TokenValue);
}
Use this:
http://docs.identityserver.io/en/latest/topics/tools.html
Use this tool that come with identity server:
Declare it in the constructor, to receive by dependecy injection.
IdentityServer4.IdentityServerTools _identityServerTools
var issuer = "http://" + httpRequest.Host.Value;
var token = await _identityServerTools.IssueJwtAsync(
30000,
issuer,
new System.Security.Claims.Claim[1]
{
new System.Security.Claims.Claim("cpf", cpf)
}
);
Here is another way to achieve this:
first create a custom grant named loginBy
public class LoginByGrant : ICustomGrantValidator
{
private readonly ApplicationUserManager _userManager;
public string GrantType => "loginBy";
public LoginByGrant(ApplicationUserManager userManager)
{
_userManager = userManager;
}
public async Task<CustomGrantValidationResult> ValidateAsync(ValidatedTokenRequest request)
{
var userId = Guid.Parse(request.Raw.Get("user_id"));
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
return await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult("user not exist"));
var userClaims = await _userManager.GetClaimsAsync(user.Id);
return
await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult(user.Id.ToString(), "custom", userClaims));
}
}
then add this custom grant in identity startup class
factory.CustomGrantValidators.Add(
new Registration<ICustomGrantValidator>(resolver => new LoginByGrant(ApplicaionUserManager)));
and finally in your api
public async Task<IHttpActionResult> LoginBy(Guid userId)
{
var tokenClient = new TokenClient(Constants.TokenEndPoint, Constants.ClientId, Constants.Secret);
var payload = new { user_id = userId.ToString() };
var result = await tokenClient.RequestCustomGrantAsync("loginBy", "customScope", payload);
if (result.IsError)
return Ok(result.Json);
return Ok(new { access_token = result.AccessToken, expires_in = result.ExpiresIn});
}
Further to my comment on your original question. Implement an impersonation feature within the implicit/hybrid flow. If a user is determined to be a "super admin" then present them with an additional step after authentication that lets them enter/select the account they wish to impersonate. Once that's done simply establish the session on the identity server as the selected user (and possibly store additional claims denoting that it is an impersonated session and who is doing the impersonation). Any tokens will then be issued as if you were that user and all without having to know the password.
Additionally if you wish to create tokens yourself have a look at the ITokenCreationService provided by IdSrv4. You can inject that into your own controller/service/whatever and use CreateTokenAsync(Token token) to generate a signed JWT with any claims you like.
A little late to answer.
in my case of Generating Access Token Without Password there was another identity server as an organization sso, and our implementation already used IdentityServer, so we need to get user token from second IdentityServer (after user login and redirected to our app), extract sub, check if it is already existed(if not insert into our local IdentityServer), finally select user and use newly grant to get token for user.
your client should have this granttype as Allowed Grant types (here userexchange):
see: identity server docs, or duende docs for more information
public class TokenExchangeGrantValidator : IExtensionGrantValidator {
protected readonly UserManager<ToranjApplicationUser> _userManager;
private readonly IEventService _events;
public TokenExchangeGrantValidator(ITokenValidator validator, IHttpContextAccessor httpContextAccessor, UserManager<ToranjApplicationUser> userManager
, IEventService events) {
_userManager = userManager;
_events = events;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context) {
var userName = context.Request.Raw.Get("uname");
if (string.IsNullOrEmpty(userName)) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var user = await _userManager.FindByNameAsync(userName);
// or use this one, if you are sending userId
//var user = await _userManager.FindByIdAsync(userId);
if (null == user) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id.ToString(), user.UserName, false, context.Request.ClientId));
var customResponse = new Dictionary<string, object>
{
{OidcConstants.TokenResponse.IssuedTokenType, OidcConstants.TokenTypeIdentifiers.AccessToken}
};
context.Result = new GrantValidationResult(
subject: user.Id.ToString(),
authenticationMethod: GrantType,
customResponse: customResponse);
}
public string GrantType => "userexchange";
}
in your startup's ConfigureServices after var builder = services.AddIdentityServer(...) add your newly created class.
builder.AddExtensionGrantValidator<TokenExchangeGrantValidator>();
calling it to get token is as simple as:
POST /connect/token
grant_type=userexchange&
scope=yourapi&
uname=yourusername&
client_id=yourClientId
client_secret=secret
I have created ASP.NET Core WebApi protected with IdentityServer4 using ROPC flow (using this example: https://github.com/robisim74/AngularSPAWebAPI).
How to manually generate access_token from the server without password?
[HttpPost("loginas/{id}")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> LoginAs(int id, [FromServices] ITokenService TS,
[FromServices] IUserClaimsPrincipalFactory<ApplicationUser> principalFactory,
[FromServices] IdentityServerOptions options)
{
var Request = new TokenCreationRequest();
var User = await userManager.FindByIdAsync(id.ToString());
var IdentityPricipal = await principalFactory.CreateAsync(User);
var IdServerPrincipal = IdentityServerPrincipal.Create(User.Id.ToString(), User.UserName, IdentityPricipal.Claims.ToArray());
Request.Subject = IdServerPrincipal;
Request.IncludeAllIdentityClaims = true;
Request.ValidatedRequest = new ValidatedRequest();
Request.ValidatedRequest.Subject = Request.Subject;
Request.ValidatedRequest.SetClient(Config.GetClients().First());
Request.Resources = new Resources(Config.GetIdentityResources(), Config.GetApiResources());
Request.ValidatedRequest.Options = options;
Request.ValidatedRequest.ClientClaims = IdServerPrincipal.Claims.ToArray();
var Token = await TS.CreateAccessTokenAsync(Request);
Token.Issuer = "http://" + HttpContext.Request.Host.Value;
var TokenValue = await TS.CreateSecurityTokenAsync(Token);
return Ok(TokenValue);
}
For a newly released IdentityServer 2.0.0 the code needs some modifications:
[HttpPost("loginas/{id}")]
[Authorize(Roles = "admin")]
public async Task<IActionResult> LoginAs(int id, [FromServices] ITokenService TS,
[FromServices] IUserClaimsPrincipalFactory<ApplicationUser> principalFactory,
[FromServices] IdentityServerOptions options)
{
var Request = new TokenCreationRequest();
var User = await userManager.FindByIdAsync(id.ToString());
var IdentityPricipal = await principalFactory.CreateAsync(User);
var IdentityUser = new IdentityServerUser(User.Id.ToString());
IdentityUser.AdditionalClaims = IdentityPricipal.Claims.ToArray();
IdentityUser.DisplayName = User.UserName;
IdentityUser.AuthenticationTime = System.DateTime.UtcNow;
IdentityUser.IdentityProvider = IdentityServerConstants.LocalIdentityProvider;
Request.Subject = IdentityUser.CreatePrincipal();
Request.IncludeAllIdentityClaims = true;
Request.ValidatedRequest = new ValidatedRequest();
Request.ValidatedRequest.Subject = Request.Subject;
Request.ValidatedRequest.SetClient(Config.GetClients().First());
Request.Resources = new Resources(Config.GetIdentityResources(), Config.GetApiResources());
Request.ValidatedRequest.Options = options;
Request.ValidatedRequest.ClientClaims = IdentityUser.AdditionalClaims;
var Token = await TS.CreateAccessTokenAsync(Request);
Token.Issuer = HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value;
var TokenValue = await TS.CreateSecurityTokenAsync(Token);
return Ok(TokenValue);
}
Use this:
http://docs.identityserver.io/en/latest/topics/tools.html
Use this tool that come with identity server:
Declare it in the constructor, to receive by dependecy injection.
IdentityServer4.IdentityServerTools _identityServerTools
var issuer = "http://" + httpRequest.Host.Value;
var token = await _identityServerTools.IssueJwtAsync(
30000,
issuer,
new System.Security.Claims.Claim[1]
{
new System.Security.Claims.Claim("cpf", cpf)
}
);
Here is another way to achieve this:
first create a custom grant named loginBy
public class LoginByGrant : ICustomGrantValidator
{
private readonly ApplicationUserManager _userManager;
public string GrantType => "loginBy";
public LoginByGrant(ApplicationUserManager userManager)
{
_userManager = userManager;
}
public async Task<CustomGrantValidationResult> ValidateAsync(ValidatedTokenRequest request)
{
var userId = Guid.Parse(request.Raw.Get("user_id"));
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
return await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult("user not exist"));
var userClaims = await _userManager.GetClaimsAsync(user.Id);
return
await Task.FromResult<CustomGrantValidationResult>(new CustomGrantValidationResult(user.Id.ToString(), "custom", userClaims));
}
}
then add this custom grant in identity startup class
factory.CustomGrantValidators.Add(
new Registration<ICustomGrantValidator>(resolver => new LoginByGrant(ApplicaionUserManager)));
and finally in your api
public async Task<IHttpActionResult> LoginBy(Guid userId)
{
var tokenClient = new TokenClient(Constants.TokenEndPoint, Constants.ClientId, Constants.Secret);
var payload = new { user_id = userId.ToString() };
var result = await tokenClient.RequestCustomGrantAsync("loginBy", "customScope", payload);
if (result.IsError)
return Ok(result.Json);
return Ok(new { access_token = result.AccessToken, expires_in = result.ExpiresIn});
}
Further to my comment on your original question. Implement an impersonation feature within the implicit/hybrid flow. If a user is determined to be a "super admin" then present them with an additional step after authentication that lets them enter/select the account they wish to impersonate. Once that's done simply establish the session on the identity server as the selected user (and possibly store additional claims denoting that it is an impersonated session and who is doing the impersonation). Any tokens will then be issued as if you were that user and all without having to know the password.
Additionally if you wish to create tokens yourself have a look at the ITokenCreationService provided by IdSrv4. You can inject that into your own controller/service/whatever and use CreateTokenAsync(Token token) to generate a signed JWT with any claims you like.
A little late to answer.
in my case of Generating Access Token Without Password there was another identity server as an organization sso, and our implementation already used IdentityServer, so we need to get user token from second IdentityServer (after user login and redirected to our app), extract sub, check if it is already existed(if not insert into our local IdentityServer), finally select user and use newly grant to get token for user.
your client should have this granttype as Allowed Grant types (here userexchange):
see: identity server docs, or duende docs for more information
public class TokenExchangeGrantValidator : IExtensionGrantValidator {
protected readonly UserManager<ToranjApplicationUser> _userManager;
private readonly IEventService _events;
public TokenExchangeGrantValidator(ITokenValidator validator, IHttpContextAccessor httpContextAccessor, UserManager<ToranjApplicationUser> userManager
, IEventService events) {
_userManager = userManager;
_events = events;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context) {
var userName = context.Request.Raw.Get("uname");
if (string.IsNullOrEmpty(userName)) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
var user = await _userManager.FindByNameAsync(userName);
// or use this one, if you are sending userId
//var user = await _userManager.FindByIdAsync(userId);
if (null == user) {
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
return;
}
await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id.ToString(), user.UserName, false, context.Request.ClientId));
var customResponse = new Dictionary<string, object>
{
{OidcConstants.TokenResponse.IssuedTokenType, OidcConstants.TokenTypeIdentifiers.AccessToken}
};
context.Result = new GrantValidationResult(
subject: user.Id.ToString(),
authenticationMethod: GrantType,
customResponse: customResponse);
}
public string GrantType => "userexchange";
}
in your startup's ConfigureServices after var builder = services.AddIdentityServer(...) add your newly created class.
builder.AddExtensionGrantValidator<TokenExchangeGrantValidator>();
calling it to get token is as simple as:
POST /connect/token
grant_type=userexchange&
scope=yourapi&
uname=yourusername&
client_id=yourClientId
client_secret=secret
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;
};
};
})
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 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.