如何验证通过cookie而不是标头传递的JWT?类似于UseCookieAuthentication,但是对于只包含JWT的cookie.
解决方法
https://stormpath.com/blog/token-authentication-asp-net-core
它们将JWT令牌存储在仅http的cookie中以防止XSS攻击.
然后,他们通过在Startup.cs中添加以下代码来验证cookie中的JWT令牌:
app.UseCookieAuthentication(new CookieAuthenticationOptions { AutomaticAuthenticate = true,AutomaticChallenge = true,AuthenticationScheme = "Cookie",CookieName = "access_token",TicketDataFormat = new CustomJwtDataFormat( SecurityAlgorithms.HmacSha256,tokenValidationParameters) });
CustomJwtDataFormat()是这里定义的自定义格式:
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(); } }
另一个解决方案是编写一些自定义中间件来拦截每个请求,查看它是否有cookie,从cookie中提取JWT并在它到达控制器的Authorize过滤器之前动态添加Authorization标头.以下是一些适用于OAuth令牌的代码,以获取想法:
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); } } }
…其中AccessToken是以下类:
public class AccessToken { public string token_type { get; set; } public string access_token { get; set; } public string expires_in { get; set; } }
希望这可以帮助.
注意:同样重要的是要注意这种做事方式(仅限http的cookie中的令牌)将有助于防止XSS攻击但是不能免受跨站点请求伪造(CSRF)攻击,因此您必须使用防伪令牌或设置自定义标头以防止这些.
此外,如果您不进行任何内容清理,攻击者仍然可以运行XSS脚本代表用户发出请求,即使启用了仅http cookie和CRSF保护.但是,攻击者无法窃取仅包含令牌的http的cookie,攻击者也无法从第三方网站发出请求.
编辑:在评论中写道,博客文章链接和代码是由OP自己在几天前提出这个问题后编写的.
对于那些对另一种“cookie中的令牌”感兴趣的方法来减少XSS暴露,他们可以使用oAuth中间件,例如ASP.NET Core中的OpenId Connect Server.
在被调用以将令牌(ApplyTokenResponse())发送回客户端的令牌提供程序的方法中,您可以序列化令牌并将其存储到仅限http的cookie中:
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.IdentityToken); // add the user's roles as claims foreach (var role in roles) { identity.AddClaim(ClaimTypes.Role,role,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); } } }
然后,您需要确保每个请求都附加了cookie.您还必须编写一些中间件来拦截cookie并将其设置为标头:
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 = "/",Secure = false,Expires = DateTime.UtcNow.AddHours(-1) }); context.Response.Redirect("/"); return; } } else { await _next.Invoke(context); } } }
在startup.cs的Configure()中:
// use the AuthorizationHeader middleware app.UseMiddleware<AuthorizationHeader>(); // Add a new middleware validating access tokens. app.USEOAuthValidation();
然后,您可以正常使用“授权”属性.
[Authorize(Roles = "Administrator,User")]
此解决方案适用于api和mvc应用程序.但是对于ajax和fetch请求,你必须编写一些自定义中间件,它不会将用户重定向到登录页面而是返回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; } } }