MVC with Owin JWT Identity - c#

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);
}
}
}

Related

C# - Azure AD - Issue with redirect after login

I'm using Azure AD Connection for integrated login in an ASP.Net website.
If I use the same code and same configurations in a brand new project, it works. When implementing in my main project, it has some unusual behaviour.
Startup.cs is the following:
public class startup
{
string clientId = System.Configuration.ConfigurationManager.AppSettings["ClientId"];
string redirectUri = System.Configuration.ConfigurationManager.AppSettings["RedirectUri"];
static string tenant = System.Configuration.ConfigurationManager.AppSettings["Tenant"];
string authority = String.Format(System.Globalization.CultureInfo.InvariantCulture, System.Configuration.ConfigurationManager.AppSettings["Authority"], tenant);
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.IdToken,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = false
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed
}
}
);
}
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
context.HandleResponse();
context.Response.Redirect("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
}
My c# code in login page:
protected void btnLogin_Click(object sender, EventArgs e)
{
SignIn();
}
public void SignIn()
{
if (!Request.IsAuthenticated)
{
HttpContext.Current.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/MainAzure.aspx" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
else
Response.Redirect("MainAzure.aspx");
}
The issue is this:
When I Press the login button in my aspx page, it opens the Microsoft login page. When I select the account and enter the password, it redirects to my redirect page (MainAzure.aspx) but without the authentication properties. Request.IsAuthenticated returns false.
Any hints?
Try changing your implementation as below approach:
Startup.cs :
public class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
/*
* Configure the OWIN middleware
*/
public void ConfigureAuth(IAppBuilder app)
{
// Required for Azure webapps, as by default they force TLS 1.2 and this project attempts 1.0
..ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(Globals.WellKnownMetadata, Globals.Tenant, Globals.DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = Globals.ClientId,
RedirectUri = Globals.RedirectUri,
PostLogoutRedirectUri = Globals.RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claim type that specifies the Name property.
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
ValidateIssuer = false
},
// Specify the scope by appending all of the scopes requested into one string (separated by a blank space)
Scope = $"openid profile offline_access {Globals.ReadTasksScope} {Globals.WriteTasksScope}"
}
);
}
/*
* On each call to Azure AD B2C, check if a policy (e.g. the profile edit or password reset policy) has been specified in the OWIN context.
* If so, use that policy when making the call. Also, don't request a code (since it won't be needed).
*/
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var policy = notification.OwinContext.Get<string>("Policy");
if (!string.IsNullOrEmpty(policy) && !policy.Equals(Globals.DefaultPolicy))
{
notification.ProtocolMessage.Scope = OpenIdConnectScope.OpenId;
notification.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(Globals.DefaultPolicy.ToLower(), policy.ToLower());
}
return Task.FromResult(0);
}
/*
* Catch any failures received by the authentication middleware and handle appropriately
*/
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
notification.Response.Redirect("/Account/ResetPassword");
}
else if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
/*
* Callback function when an authorization code is received
*/
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
try
{
/*
The `MSALPerUserMemoryTokenCache` is created and hooked in the `UserTokenCache` used by `IConfidentialClientApplication`.
At this point, if you inspect `ClaimsPrinciple.Current` you will notice that the Identity is still unauthenticated and it has no claims,
but `MSALPerUserMemoryTokenCache` needs the claims to work properly. Because of this sync problem, we are using the constructor that
receives `ClaimsPrincipal` as argument and we are getting the claims from the object `AuthorizationCodeReceivedNotification context`.
This object contains the property `AuthenticationTicket.Identity`, which is a `ClaimsIdentity`, created from the token received from
Azure AD and has a full set of claims.
*/
IConfidentialClientApplication confidentialClient = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(notification.AuthenticationTicket.Identity));
// Upon successful sign in, get & cache a token using MSAL
AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(Globals.Scopes, notification.Code).ExecuteAsync();
}
catch (Exception ex)
{
throw new HttpResponseException(new HttpResponseMessage
{
StatusCode = HttpStatusCode.BadRequest,
ReasonPhrase = $"Unable to get authorization code {ex.Message}."
});
}
}
(For user authentication activities) AccountController.cs :
public class AccountController : Controller
{
/*
* Called when requesting to sign up or sign in
*/
public void SignUpSignIn(string redirectUrl)
{
redirectUrl = redirectUrl ?? "/";
// Use the default policy to process the sign up / sign in flow
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = redirectUrl });
return;
}
/*
* Called when requesting to edit a profile
*/
public void EditProfile()
{
if (Request.IsAuthenticated)
{
// Let the middleware know you are trying to use the edit profile policy (see OnRedirectToIdentityProvider in Startup.Auth.cs)
HttpContext.GetOwinContext().Set("Policy", Globals.EditProfilePolicyId);
// Set the page to redirect to after editing the profile
var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
return;
}
Response.Redirect("/");
}
/*
* Called when requesting to reset a password
*/
public void ResetPassword()
{
// Let the middleware know you are trying to use the reset password policy (see OnRedirectToIdentityProvider in Startup.Auth.cs)
HttpContext.GetOwinContext().Set("Policy", Globals.ResetPasswordPolicyId);
// Set the page to redirect to after changing passwords
var authenticationProperties = new AuthenticationProperties { RedirectUri = "/" };
HttpContext.GetOwinContext().Authentication.Challenge(authenticationProperties);
return;
}
/*
* Called when requesting to sign out
*/
public async Task SignOut()
{
// To sign out the user, you should issue an OpenIDConnect sign out request.
if (Request.IsAuthenticated)
{
await MsalAppBuilder.ClearUserTokenCache();
IEnumerable<AuthenticationDescription> authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
Request.GetOwinContext().Authentication.GetAuthenticationTypes();
}
}
}
For complete code, you can visit Azure AD B2C: Call an ASP.NET Web API from an ASP.NET Web App

Only validate JWT if bearer header is present

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;
}

How can I validate a JWT passed via cookies?

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;
};
};
})

mvc custom oauth server does not redirect to login page on unauthorized

I setup my own oauth authorization Server (NO Login via Facebook, Google and co). Unfortunately my server does not redirect to the login page after the Authorize-Endpoint is hit. Why doesn't the middleware redirect to my login page?
I followed some tutorials:
OWIN OAuth 2.0 Authorization Server
OAuth custom provider c# (Which is by the way the only code example i found for client login into custom oauth server. Thanks to #MatthiasRamp. All other stuff I read is about how to login with the social media Clients which is very frustrating)
MVC 5 application - implement OAuth Authorization code flow
This is what I made shortend on my oauth server side:
Startup.Auth.cs use active Cookie Authentication with Loginpath
Startup.Auth.cs use passive Cookie Authentication
Startup.Auth.cs use authorization Server with AuthorizeEndpointPath
SecurityController/Authorize calls Authentication.challenge() to change the status to 401
One point in the post from #Satish P he described:
Redirect the client to a login page
For this purpose I set the property CookieAuthenticationOptions.LoginPath which tells me
The LoginPath property informs the middleware that it should change an outgoing 401 Unauthorized status code into a 302 redirection onto the given login path.
This is my Startup.Auth.cs including LoginPath:
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
// and to use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Application",
AuthenticationMode = AuthenticationMode.Passive,
LoginPath = new PathString("/Security/Login"),
LogoutPath = new PathString("/Security/Logout")
});
// Enable the External Sign In Cookie.
app.SetDefaultSignInAsAuthenticationType("External");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "External",
AuthenticationMode = AuthenticationMode.Passive,
CookieName = CookieAuthenticationDefaults.CookiePrefix + "External",
ExpireTimeSpan = TimeSpan.FromMinutes(5),
});
// The UseOAuthAuthorizationServer extension method is to setup the authorization server. The setup options are: [...]
app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
{
AuthorizeEndpointPath = new PathString("/Security/Authorize"),
TokenEndpointPath = new PathString("/Token"),
ApplicationCanDisplayErrors = true,
#if DEBUG
AllowInsecureHttp = true,
#endif
// Authorization server provider which controls the lifecycle of Authorization Server
Provider = new ApplicationOAuthProvider(PublicClientId)
});
}
I created a class which derives from OAuthAuthorizationServerProvider.
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
// [Check username and pw here]
var oAuthIdentity = new ClaimsIdentity(new GenericIdentity(context.UserName, OAuthDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
var cookiesIdentity = new ClaimsIdentity(new GenericIdentity(context.UserName, CookieAuthenticationDefaults.AuthenticationType), context.Scope.Select(x => new Claim("urn:oauth:scope", x)));
AuthenticationProperties properties = CreateProperties(context.UserName);
AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
context.Validated(ticket);
context.Request.Context.Authentication.SignIn(cookiesIdentity);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
var grantType = context.Parameters.SingleOrDefault(p => p.Key == "grant_type").Value;
if (grantType != null)
{
if (grantType[0] == "authorization_code")
{
string clientId;
string clientSecret;
if (context.TryGetBasicCredentials(out clientId, out clientSecret) || context.TryGetFormCredentials(out clientId, out clientSecret))
{
if (clientId == Clients.ClientApp.Id && clientSecret == Clients.ClientApp.Secret)
{
context.Validated();
}
}
}
}
return Task.FromResult<object>(null);
}
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == Clients.ClientApp.Id)
{
context.Validated(Clients.ClientApp.RedirectUrl);
}
return Task.FromResult<object>(null);
}
public static AuthenticationProperties CreateProperties(string userName)
{
IDictionary<string, string> data = new Dictionary<string, string>
{
{ "userName", userName }
};
return new AuthenticationProperties(data);
}
}
And this is the where the redirect should happen because challenge will change the response to unauthorized (401). Instead of the redirect to the login page it wants to return the authorize view.
public ActionResult Authorize()
{
if (Response.StatusCode != 200)
{
return View("AuthorizeError");
}
var authType = DefaultAuthenticationTypes.ApplicationCookie;
var authentication = HttpContext.GetOwinContext().Authentication;
var ticket = authentication.AuthenticateAsync(authType).Result;
var identity = ticket != null ? ticket.Identity : null;
if (identity == null)
{
authentication.Challenge(authType);
}
else
{
// login stuff
}
return View();
}
I know it's too late and you might not need this anymore but just in case if anyone else also faces this issue then you need to return HttpUnauthorizedResult(); after challenge and it will rediret you to login page.
e.g
if (identity == null)
{
authentication.Challenge(authType);
return new HttpUnauthorizedResult();
}
//login stuff

Return error on invalid or expired token

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.

Categories