Предлагаю вам взглянуть на следующую ссылку.
https://stormpath.com/blog/token-authentication-asp-net-core
Они хранят JWT маркер в HTTP только печенье для предотвращения XSS атак.
Они затем проверить маркер JWT в куки, добавив следующий код в Startup.cs:
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, извлекает JWT из файла cookie и добавляет заголовок авторизации на лету, прежде чем он достигнет фильтра Authorize ваших контроллеров. Вот код, который работает для 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);
}
}
}
... где маркер доступ является следующим классом:
public class AccessToken
{
public string token_type { get; set; }
public string access_token { get; set; }
public string expires_in { get; set; }
}
Надеется, что это помогает.
ПРИМЕЧАНИЕ. Важно также отметить, что этот способ делать вещи (токен в http только cookie) поможет предотвратить атаки XSS, но, тем не менее, не защищает от атак Cross-Site Request Forgery (CSRF), поэтому вы также должны использовать анти-подделки или установить специальные заголовки, чтобы предотвратить их.
Кроме того, если вы не будете заниматься какой-либо санитацией контента, злоумышленник все равно сможет запустить сценарий XSS для выполнения запросов от имени пользователя, даже с включенными только cookie-файлами http и защитой CRSF. Однако злоумышленник не сможет украсть только файлы cookie http, содержащие токены, и злоумышленник не сможет делать запросы с стороннего веб-сайта.
Поэтому вы должны по-прежнему выполнять тяжелую санитарную на пользовательский контент, такие как комментарии и т.д. ...
EDIT: Это было написано в комментариях, что пост в блоге связаны между собой и код были написаны самим О.П. несколько дней назад, задав этот вопрос.
Для тех, кто заинтересован в другом методе «токена в cookie» для уменьшения воздействия XSS, они могут использовать промежуточное ПО OAuth, такое как OpenId Connect Server в ASP.NET Core.
В методе маркеров поставщика, который вызывается для отправки фишки обратно (ApplyTokenResponse()) клиента можно сериализовать маркер и хранить его в печенье, который является HTTP только:
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);
}
}
}
Затем вам нужно убедиться, что каждый запрос имеет прикрепленный к нему файл 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 = "/",
HttpOnly = true,
Secure = false,
Expires = DateTime.UtcNow.AddHours(-1)
});
context.Response.Redirect("/");
return;
}
}
else
{
await _next.Invoke(context);
}
}
}
В Configure() из startup.cs:
// use the AuthorizationHeader middleware
app.UseMiddleware<AuthorizationHeader>();
// Add a new middleware validating access tokens.
app.UseOAuthValidation();
Вы можете использовать атрибут Авторизоваться нормально.
[Authorize(Roles = "Administrator,User")]
Это решение работает как для приложений api, так и для mvc. Для AJAX и тем не менее получать запросы ваши должны написать некоторые пользовательские промежуточное программное обеспечение, которое не будет перенаправлять пользователя на страницу входа в систему, и вместо возвращал 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;
}
}
}
Любопытно: в чем смысл использования токенов-носителей, если вы хотите использовать файлы cookie для их переноса? Весь смысл использования токенов-носителей вместо куки-файлов - избегать проблем безопасности, таких как атаки XSRF. Если вы повторно вводите файлы cookie в уравнение, вы снова вводите свою модель угрозы. – Pinpoint
@Pinpoint JWTs - это не только маркеры-носители; они могут использоваться либо через заголовок несущей, либо через файлы cookie. Я использую JWT для сеансов без состояния, но сохраняю их в файлах cookie, потому что поддержка браузера проста. XSS смягчается флагами cookie. –
1. По определению, JWT являются либо знаками на предъявителя, либо PoP (в первом случае вам не нужно доказывать, что вы являетесь законным обладателем токена, во втором - вам нужно предоставить серверу доказательство владение). 2. Использование JWT для представления «сеанса» и хранения их в файле cookie для проверки подлинности (который сам по себе является «сеансом») не имеет смысла, я боюсь. 3. XSS не имеет ничего общего с XSRF, это совершенно другая угроза. – Pinpoint