JWT-аутентификация для ASP.NET Web API
Я пытаюсь поддерживать токен-носитель JWT (JSON Web Token) в своем приложении веб-API, и я теряюсь.
Я вижу поддержку .NET Core и приложений OWIN.
В настоящее время я размещаю свое приложение в IIS.
Как я могу добиться этого модуля аутентификации в моем приложении? Можно ли каким-либо образом использовать конфигурацию <authentication>
аналогичную использованию форм/аутентификации Windows?
Ответы
Ответ 1
Я ответил на этот вопрос: как обеспечить безопасность ASP.NET Web API 4 года назад с помощью HMAC.
Сейчас многое изменилось в сфере безопасности, особенно JWT становится популярным. Здесь я попытаюсь объяснить, как использовать JWT самым простым и простым способом, каким только могу, чтобы мы не потерялись в джунглях OWIN, Oauth2, ASP.NET Identity... :).
Если вы не знаете токен JWT, вам нужно немного взглянуть на:
https://tools.ietf.org/html/rfc7519
По сути, токен JWT выглядит так:
<base64-encoded header>.<base64-encoded claims>.<base64-encoded signature>
Пример:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1NzI0LCJleHAiOjE0Nzc1NjY5MjQsImlhdCI6MTQ3NzU2NTcyNH0.6MzD1VwA5AcOcajkFyKhLYybr3h13iZjDyHm9zysDFQ
Токен JWT состоит из трех разделов:
- Заголовок: формат JSON, который закодирован в Base64
- Заявки: формат JSON, закодированный в Base64.
- Подпись: создается и подписывается на основе заголовка и утверждений, которые закодированы в Base64.
Если вы используете веб-сайт jwt.io с токеном, указанным выше, вы можете декодировать токен и видеть его, как показано ниже:
![enter image description here]()
Технически, JWT использует подпись, которая подписана из заголовков и требует алгоритма безопасности, указанного в заголовках (пример: HMACSHA256). Поэтому JWT необходимо передавать по HTTP, если вы храните какую-либо конфиденциальную информацию в претензиях.
Теперь, чтобы использовать аутентификацию JWT, вам не нужно промежуточное ПО OWIN, если у вас есть устаревшая система Web Api. Простая концепция заключается в том, как предоставить токен JWT и как проверить токен при поступлении запроса. Это.
Возвращаясь к демонстрации, чтобы облегчить токен JWT, я сохраняю только username
и expiration time
в JWT. Но таким образом, вы должны пересоздать новую локальную идентификацию (принципал), чтобы добавить больше информации, например: роли... если вы хотите выполнить авторизацию ролей. Но если вы хотите добавить больше информации в JWT, вам решать: она очень гибкая.
Вместо использования промежуточного программного обеспечения OWIN вы можете просто предоставить конечную точку токена JWT, используя действие контроллера:
public class TokenController : ApiController
{
// This is naive endpoint for demo, it should use Basic authentication
// to provide token or POST request
[AllowAnonymous]
public string Get(string username, string password)
{
if (CheckUser(username, password))
{
return JwtManager.GenerateToken(username);
}
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
public bool CheckUser(string username, string password)
{
// should check in the database
return true;
}
}
Это наивное действие; в производстве вы должны использовать запрос POST или конечную точку базовой аутентификации для предоставления токена JWT.
Как сгенерировать токен на основе username
?
Вы можете использовать пакет NuGet под названием System.IdentityModel.Tokens.Jwt
от Microsoft, чтобы сгенерировать токен или даже другой пакет, если хотите. В демоверсии я использую HMACSHA256
с SymmetricKey
:
/// <summary>
/// Use the below code to generate symmetric Secret Key
/// var hmac = new HMACSHA256();
/// var key = Convert.ToBase64String(hmac.Key);
/// </summary>
private const string Secret = "db3OIsj+BXE9NZDy0t8W3TcNekrF+2d/1sFnWG4HnV8TZY30iTOdtVWJG8abWvB1GlOgJuQZdcF2Luqm/hccMw==";
public static string GenerateToken(string username, int expireMinutes = 20)
{
var symmetricKey = Convert.FromBase64String(Secret);
var tokenHandler = new JwtSecurityTokenHandler();
var now = DateTime.UtcNow;
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username)
}),
Expires = now.AddMinutes(Convert.ToInt32(expireMinutes)),
SigningCredentials = new SigningCredentials(
new SymmetricSecurityKey(symmetricKey),
SecurityAlgorithms.HmacSha256Signature)
};
var stoken = tokenHandler.CreateToken(tokenDescriptor);
var token = tokenHandler.WriteToken(stoken);
return token;
}
Конечная точка для предоставления токена JWT готова. Теперь, как проверить JWT, когда приходит запрос? В демоверсии я построил JwtAuthenticationAttribute
который наследуется от IAuthenticationFilter
(подробнее о фильтре аутентификации здесь).
С помощью этого атрибута вы можете аутентифицировать любое действие: вам просто нужно поместить этот атрибут в это действие.
public class ValueController : ApiController
{
[JwtAuthentication]
public string Get()
{
return "value";
}
}
Вы также можете использовать промежуточное программное обеспечение OWIN или DelegateHander, если вы хотите проверить все входящие запросы для вашего WebAPI (не относится к контроллеру или действию)
Ниже приведен основной метод из фильтра аутентификации:
private static bool ValidateToken(string token, out string username)
{
username = null;
var simplePrinciple = JwtManager.GetPrincipal(token);
var identity = simplePrinciple.Identity as ClaimsIdentity;
if (identity == null)
return false;
if (!identity.IsAuthenticated)
return false;
var usernameClaim = identity.FindFirst(ClaimTypes.Name);
username = usernameClaim?.Value;
if (string.IsNullOrEmpty(username))
return false;
// More validate to check whether username exists in system
return true;
}
protected Task<IPrincipal> AuthenticateJwtToken(string token)
{
string username;
if (ValidateToken(token, out username))
{
// based on username to get more information from database
// in order to build local identity
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, username)
// Add more claims if needed: Roles, ...
};
var identity = new ClaimsIdentity(claims, "Jwt");
IPrincipal user = new ClaimsPrincipal(identity);
return Task.FromResult(user);
}
return Task.FromResult<IPrincipal>(null);
}
Рабочий процесс заключается в использовании библиотеки JWT (пакет NuGet выше) для проверки токена JWT, а затем возврата обратно ClaimsPrincipal
. Вы можете выполнить дополнительную проверку, например, проверить, существует ли пользователь в вашей системе, и добавить другие пользовательские проверки, если хотите. Код для проверки токена JWT и возврата принципала:
public static ClaimsPrincipal GetPrincipal(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken;
if (jwtToken == null)
return null;
var symmetricKey = Convert.FromBase64String(Secret);
var validationParameters = new TokenValidationParameters()
{
RequireExpirationTime = true,
ValidateIssuer = false,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(symmetricKey)
};
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);
return principal;
}
catch (Exception)
{
//should write log
return null;
}
}
Если токен JWT проверен, а принципал возвращен, вы должны создать новый локальный идентификатор и добавить в него дополнительную информацию для проверки авторизации роли.
Не забудьте добавить config.Filters.Add(new AuthorizeAttribute());
(авторизация по умолчанию) в глобальном масштабе, чтобы предотвратить любой анонимный запрос к вашим ресурсам.
Вы можете использовать Почтальон для тестирования демо:
Запросить токен (наивно, как я уже говорил выше, только для демонстрации):
GET http://localhost:{port}/api/token?username=cuong&password=1
Поместите токен JWT в заголовок для авторизованного запроса, например:
GET http://localhost:{port}/api/value
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImN1b25nIiwibmJmIjoxNDc3NTY1MjU4LCJleHAiOjE0Nzc1NjY0NTgsImlhdCI6MTQ3NzU2NTI1OH0.dSwwufd4-gztkLpttZsZ1255oEzpWCJkayR_4yvNL1s
Демо-версия размещена здесь: https://github.com/cuongle/WebApi.Jwt.
Ответ 2
Мне удалось добиться этого с минимальными усилиями (так же просто, как с ASP.NET Core).
Для этого я использую Owin Startup.cs
файл и Microsoft.Owin.Security.Jwt
библиотеку.
Чтобы приложение Startup.cs
нам нужно изменить Web.config
:
<configuration>
<appSettings>
<add key="owin:AutomaticAppStartup" value="true" />
...
Вот как должен выглядеть Startup.cs
:
using MyApp.Helpers;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Jwt;
using Owin;
[assembly: OwinStartup(typeof(MyApp.App_Start.Startup))]
namespace MyApp.App_Start
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
ValidAudience = ConfigHelper.GetAudience(),
ValidIssuer = ConfigHelper.GetIssuer(),
IssuerSigningKey = ConfigHelper.GetSymmetricSecurityKey(),
ValidateLifetime = true,
ValidateIssuerSigningKey = true
}
});
}
}
}
Многие из вас, ребята, используют ASP.NET Core в настоящее время, так что, как вы можете видеть, он не сильно отличается от того, что у нас там есть.
Сначала это действительно озадачило меня, я пытался внедрить пользовательских провайдеров и т.д. Но я не ожидал, что это будет так просто. OWIN
просто качается!
Стоит упомянуть одну вещь - после того, как я включил OWIN Startup, библиотека NSWag
перестала работать для меня (например, некоторые из вас могут захотеть автоматически генерировать HTTP-прокси для машинописного текста для приложения Angular).
Решение также было очень простым - я заменил NSWag
на Swashbuckle
и у меня больше не было проблем.
Хорошо, теперь ConfigHelper
кодом ConfigHelper
:
public class ConfigHelper
{
public static string GetIssuer()
{
string result = System.Configuration.ConfigurationManager.AppSettings["Issuer"];
return result;
}
public static string GetAudience()
{
string result = System.Configuration.ConfigurationManager.AppSettings["Audience"];
return result;
}
public static SigningCredentials GetSigningCredentials()
{
var result = new SigningCredentials(GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256);
return result;
}
public static string GetSecurityKey()
{
string result = System.Configuration.ConfigurationManager.AppSettings["SecurityKey"];
return result;
}
public static byte[] GetSymmetricSecurityKeyAsBytes()
{
var issuerSigningKey = GetSecurityKey();
byte[] data = Encoding.UTF8.GetBytes(issuerSigningKey);
return data;
}
public static SymmetricSecurityKey GetSymmetricSecurityKey()
{
byte[] data = GetSymmetricSecurityKeyAsBytes();
var result = new SymmetricSecurityKey(data);
return result;
}
public static string GetCorsOrigins()
{
string result = System.Configuration.ConfigurationManager.AppSettings["CorsOrigins"];
return result;
}
}
Еще один важный аспект - я отправил токен JWT через заголовок авторизации, поэтому машинописный код выглядит для меня следующим образом:
(код ниже генерируется NSWag)
@Injectable()
export class TeamsServiceProxy {
private http: HttpClient;
private baseUrl: string;
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) {
this.http = http;
this.baseUrl = baseUrl ? baseUrl : "https://localhost:44384";
}
add(input: TeamDto | null): Observable<boolean> {
let url_ = this.baseUrl + "/api/Teams/Add";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(input);
let options_ : any = {
body: content_,
observe: "response",
responseType: "blob",
headers: new HttpHeaders({
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer " + localStorage.getItem('token')
})
};
См. Часть заголовков - "Authorization": "Bearer " + localStorage.getItem('token')
Ответ 3
Я думаю, вы должны использовать какой-то сторонний сервер для поддержки токена JWT, и нет никакой поддержки JWT в WEB API 2.
Однако существует проект OWIN для поддержки некоторого формата подписанного токена (не JWT). Он работает как сокращенный протокол OAuth, чтобы предоставить просто простую форму аутентификации для веб-сайта.
Вы можете узнать больше об этом, например. здесь.
Это довольно долго, но большинство деталей - это детали с контроллерами и идентификаторами ASP.NET, которые вам могут вообще не нужны. Наиболее важными являются
Шаг 9: добавьте поддержку для создания токенов маркера OAuth
Шаг 12: Тестирование Back-end API
Здесь вы можете прочитать, как настроить конечную точку (например, "/токен" ), с которой вы можете получить доступ из интерфейса (и сведения о формате запроса).
Другие шаги содержат сведения о том, как подключить эту конечную точку к базе данных и т.д., и вы можете выбрать нужные вам детали.
Ответ 4
Я также реализую API-интерфейс Jason Web Token API в своем проекте, вы можете скачать с помощью этой ссылки JWT API Token. Вы можете использовать [authorize]
для проверки подлинности пользователя или нет?
Ответ 5
Есть сообщение в блоге, в котором объясняется, как использовать JWT, не используя все эти сложные вещи OAuth, и не беспокоясь о проблеме, которую OWIN имеет с обычным классом HttpContext.
Проверьте это.
Ответ 6
Здесь очень минимальная и безопасная реализация аутентификации на основе утверждений с использованием токена JWT в ASP.NET Core Web API.
Прежде всего, вам нужно предоставить конечную точку, которая возвращает токен JWT с утверждениями, назначенными пользователю:
/// <summary>
/// Login provides API to verify user and returns authentication token.
/// API Path: api/account/login
/// </summary>
/// <param name="paramUser">Username and Password</param>
/// <returns>{Token: [Token] }</returns>
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] UserRequestVM paramUser, CancellationToken ct)
{
var result = await UserApplication.PasswordSignInAsync(paramUser.Email, paramUser.Password, false, lockoutOnFailure: false);
if (result.Succeeded)
{
UserRequestVM request = new UserRequestVM();
request.Email = paramUser.Email;
ApplicationUser UserDetails = await this.GetUserByEmail(request);
List<ApplicationClaim> UserClaims = await this.ClaimApplication.GetListByUser(UserDetails);
var Claims = new ClaimsIdentity(new Claim[]
{
new Claim(JwtRegisteredClaimNames.Sub, paramUser.Email.ToString()),
new Claim(UserId, UserDetails.UserId.ToString())
});
//Adding UserClaims to JWT claims
foreach (var item in UserClaims)
{
Claims.AddClaim(new Claim(item.ClaimCode, string.Empty));
}
var tokenHandler = new JwtSecurityTokenHandler();
// this information will be retrived from you Configuration
//I have injected Configuration provider service into my controller
var encryptionkey = Configuration["Jwt:Encryptionkey"];
var key = Encoding.ASCII.GetBytes(encryptionkey);
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = Configuration["Jwt:Issuer"],
Subject = Claims,
// this information will be retrived from you Configuration
//I have injected Configuration provider service into my controller
Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(Configuration["Jwt:ExpiryTimeInMinutes"])),
//algorithm to sign the token
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
return Ok(new
{
token = tokenString
});
}
return BadRequest("Wrong Username or password");
}
Теперь вам нужно добавить Аутентификацию в ваши службы в ваших ConfigureServices
внутри вашего startup.cs, чтобы добавить аутентификацию JWT в качестве службы аутентификации по умолчанию, например:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
cfg.RequireHttpsMetadata = false;
cfg.SaveToken = true;
cfg.TokenValidationParameters = new TokenValidationParameters()
{
//ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWT:Encryptionkey"])),
ValidateAudience = false,
ValidateLifetime = true,
ValidIssuer = configuration["Jwt:Issuer"],
//ValidAudience = Configuration["Jwt:Audience"],
//IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Key"])),
};
});
Теперь вы можете добавить политики к вашим сервисам авторизации, например так:
services.AddAuthorization(options =>
{
options.AddPolicy("YourPolicyNameHere",
policy => policy.RequireClaim("YourClaimNameHere"));
});
АЛЬТЕРНАТИВНО, Вы также можете (не обязательно) заполнять все свои заявки из вашей базы данных, поскольку они будут запускаться только один раз при запуске приложения и добавлять их в политики, подобные этой:
services.AddAuthorization(async options =>
{
var ClaimList = await claimApplication.GetList(applicationClaim);
foreach (var item in ClaimList)
{
options.AddPolicy(item.ClaimCode, policy => policy.RequireClaim(item.ClaimCode));
}
});
Теперь вы можете установить фильтр политики для любого из методов, которые вы хотите авторизовать, например:
[HttpPost("update")]
[Authorize(Policy = "ACC_UP")]
public async Task<IActionResult> Update([FromBody] UserRequestVM requestVm, CancellationToken ct)
{
//your logic goes here
}
Надеюсь это поможет