Начал изучать asp.net core и первое что пытался найти это некое подобие «OAuthAuthorizationServerProvider» для реализации генерации тикета и «IAuthenticationTokenProvider» для реализаций рефреш токена как в обычном asp.net, но не нашел. Не исключено, что плохо искал, и может появится коммент типа «вот обкатанная библиотека для этого дела».
Хорошо, что можно довольно просто написать свой Middleware для обработки запросов, и связать его с некоторой реализацией провайдера через «сервисы» в ConfigureServices.
Итак, мне нужно что бы были методы для получения токена по логину и паролю по адресу "/token" и метод по получению токена по рефреш-токену.
public interface IOAuthProvider
{
Task ByPassword(OAuthProviderContext oAuthProviderContext);
Task ByRefreshToken(OAuthProviderContext oAuthProviderContext);
}
То есть в конечном итоге реализовав один этот интерфейс и связав его в ConfigureServices авторизация должна работать.
В параметре «OAuthProviderContext» будут храниться данные контекста для авторизации:
public class OAuthProviderContext
{
public bool HasError { get; private set; }
public string Error { get; private set; }
public string AccessToken { get; private set; }
public string RefreshToken { get; set; }
public string ClientId { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public void SetError(string error)
{
Error = error;
HasError = true;
}
public void SetToken(string access_token, string refresh_token)
{
AccessToken = access_token;
RefreshToken = refresh_token;
}
}
Теперь надо сделать middleware, которое будет работать с будущими реализациями IOAuthProvider:
class OAuthProviderMiddleware
{
RequestDelegate _next;
IOAuthProvider _oAuthProvider;
public OAuthProviderMiddleware(RequestDelegate next, IOAuthProvider oAuthProvider)
{
_next = next;
_oAuthProvider = oAuthProvider;
}
public async Task Invoke(HttpContext context)
{
OAuthProviderContext _oAuthProviderContext;
string path = context.Request.Path.Value.ToLower().Trim();
bool isPost = context.Request.Method.ToLower() == "post";
if (path == "/token" && isPost)
{
var form = context.Request.Form;
if (!form.ContainsKey("grant_type")) {
await context.BadRequest("invalid grant_type");
return;
}
if (!form.ContainsKey("client_id"))
{
await context.BadRequest("invalid client_id");
return;
}
string grant_type = form["grant_type"];
string client_id = form["client_id"];
switch (grant_type)
{
case "password":
{
if (!form.ContainsKey("username"))
{
await context.BadRequest("invalid username");
return;
}
if (!form.ContainsKey("password"))
{
await context.BadRequest("invalid password");
return;
}
string username = form["username"];
string password = form["password"];
_oAuthProviderContext = new OAuthProviderContext()
{
ClientId = client_id,
Username = username,
Password = password
};
await _oAuthProvider.ByPassword(_oAuthProviderContext);
if (_oAuthProviderContext.HasError)
{
await context.BadRequest(_oAuthProviderContext.Error);
return;
}
else
{
await context.WriteToken(_oAuthProviderContext);
return;
}
};
case "refresh_token":
{
if (!form.ContainsKey("refresh_token"))
{
await context.BadRequest("invalid refresh_token");
return;
}
string refresh_token = form["refresh_token"];
_oAuthProviderContext = new OAuthProviderContext()
{
ClientId = client_id,
RefreshToken = refresh_token
};
await _oAuthProvider.ByRefreshToken(_oAuthProviderContext);
if (_oAuthProviderContext.HasError)
{
await context.BadRequest(_oAuthProviderContext.Error);
return;
}
else
{
await context.WriteToken(_oAuthProviderContext);
return;
}
};
default:
{
await context.BadRequest("invalid grant_type");
return;
};
}
}
else
{
await _next.Invoke(context);
}
}
}
public static class OAuthExtensions
{
public static IApplicationBuilder UseOAuth(this IApplicationBuilder builder)
{
return builder.UseMiddleware<OAuthProviderMiddleware>();
}
internal static async Task BadRequest(this HttpContext context, string Error)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsync(Error);
}
internal static async Task WriteToken(this HttpContext context, OAuthProviderContext _oAuthProviderContext)
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonConvert.SerializeObject(new
{
access_token = _oAuthProviderContext.AccessToken,
refresh_token = _oAuthProviderContext.RefreshToken
}));
}
}
В Configure потом можно будет вызвать обертку
app.UseOAuth();
Далее остается написать конкретную реализацию IOAuthProvider.
Токен будет в виде JWT, а рефреш-токен рандомный byte[] массив длиной 100 представленный как Base64. В конце еще будет ссылка на код.
public class OAuthProviderImplement : IOAuthProvider
{
IServiceProvider _services;
IOptions<AuthOptions> _authOptions = null;
Helper _helper = null;
public OAuthProviderImplement(IServiceProvider services, IOptions<AuthOptions> authOptions, Helper helper)
{
_services = services;
_authOptions = authOptions;
_helper = helper;
}
public async Task ByPassword(OAuthProviderContext context)
{
ClaimsIdentity identity = await GetIdentity(context.Username, context.ClientId, context.Password);
if (identity == null)
{
context.SetError("User not found");
return;
}
string encodedJwt = CreateJWT(identity);
string refresh_token = await CreateRefreshToken(context.ClientId, identity);
if (refresh_token == null)
{
context.SetError("Error while create refresh token");
return;
}
context.SetToken(encodedJwt, refresh_token);
return;
}
public async Task ByRefreshToken(OAuthProviderContext context)
{
ProtectedTicket protectedTicket = await GrantRefreshToken(context.RefreshToken);
if (protectedTicket == null)
{
context.SetError("Invalid refresh token");
return;
}
if (protectedTicket.clientid != context.ClientId)
{
context.SetError("Invalid client id");
return;
}
ClaimsIdentity identity = await GetIdentity(protectedTicket.username, protectedTicket.clientid);
if (identity == null)
{
context.SetError("User not found");
return;
}
string encodedJwt = CreateJWT(identity);
context.SetToken(encodedJwt, context.RefreshToken);
return;
}
string CreateJWT(ClaimsIdentity identity)
{
var now = DateTime.UtcNow;
// создаем JWT-токен
var jwt = new JwtSecurityToken(
issuer: _authOptions.Value.Issuer,
audience: _authOptions.Value.Audience,
notBefore: now,
claims: identity.Claims,
expires: now.Add(TimeSpan.FromSeconds(_authOptions.Value.LifetimeSeconds)),
signingCredentials: new SigningCredentials(_authOptions.Value.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
return encodedJwt;
}
async Task<ClaimsIdentity> GetIdentity(string username, string clientid, string password = null)
{
using (var serviceScope = _services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
IAuthRepository _repo = serviceScope.ServiceProvider.GetService<IAuthRepository>();
ApplicationUser user = null;
if (password != null)
{
user = await _repo.FindUser(username, password);
}
else
{
user = await _repo.FindUser(username);
}
if (user != null)
{
var claims = new List<Claim>
{
new Claim(ClaimsIdentity.DefaultNameClaimType, user.UserName)
};
foreach (var r in await _repo.GetRoles(user))
{
claims.Add(new Claim(ClaimsIdentity.DefaultRoleClaimType, r));
}
claims.Add(new Claim("client_id", clientid));
ClaimsIdentity claimsIdentity = new ClaimsIdentity(
claims,
"Password",
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
return claimsIdentity;
}
else
{
}
// если пользователя не найдено
return null;
}
}
async Task<string> CreateRefreshToken(string clientid, ClaimsIdentity claimsIdentity)
{
using (var serviceScope = _services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
IAuthRepository _repo = serviceScope.ServiceProvider.GetService<IAuthRepository>();
Client client = _repo.FindClient(clientid);
var refreshTokenId = _helper.GetHash(_helper.GenerateRandomCryptographicKey(100));
var refreshTokenLifeTime = client.RefreshTokenLifeTime;
var now = DateTime.UtcNow;
var token = new RefreshToken()
{
Id = _helper.GetHash(refreshTokenId),
ClientId = clientid,
Subject = claimsIdentity.Name,
IssuedUtc = now,
ExpiresUtc = now.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
};
token.ProtectedTicket = JsonConvert.SerializeObject(new ProtectedTicket { clientid = clientid, username = claimsIdentity.Name });
var result = await _repo.AddRefreshToken(token);
if (result)
{
return refreshTokenId;
}
return null;
}
}
async Task<ProtectedTicket> GrantRefreshToken(string refreshTokenId)
{
using (var serviceScope = _services.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
IAuthRepository _repo = serviceScope.ServiceProvider.GetService<IAuthRepository>();
string hashedTokenId = _helper.GetHash(refreshTokenId);
ProtectedTicket protectedTicket = null;
var refreshToken = await _repo.FindRefreshToken(hashedTokenId);
if (refreshToken != null)
{
//Get protectedTicket from refreshToken class
protectedTicket = JsonConvert.DeserializeObject<ProtectedTicket>(refreshToken.ProtectedTicket);
return protectedTicket;
}
else
{
return null;
}
}
}
}
Теперь обработаются запросы:
post "/token"
с данными в body: grant_type="password", client_id="ngAuth", username="admin", password="123"
и
post "/token"
с данными в body: grant_type="refresh_token", client_id="ngAuth", refresh_token="dgDrVQHylvHmi8QZ5oThVjWyqdrLYKhp1/XHsIJI65g="
На этом все, вообщем напишите кто че думает.
Автор: Аскар