Хорошо, что можно довольно просто написать свой 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="
На этом все, вообщем напишите кто че думает.
Весь код
Комментарии (14)
lair
23.09.2018 00:59Не исключено, что плохо искал, и может появится коммент типа «вот обкатанная библиотека для этого дела».
Ну так IdentityServer же.
Ascar Автор
23.09.2018 16:51Возможно я переборщил с названием, вообщем переименовал, так как мне не надо ouath 2.0, а по сути просто вход/токен/рефреш-токен. Про IdentityServer я знаю.
lair
23.09.2018 19:36Не надо изобретать собственную авторизацию на основе токена, когда есть OAuth 2, поэтому все равно берите IdentityServer. Там нужная вам функциональность есть, и она реализована с минимальным количеством ошибок.
gerod
23.09.2018 20:51а есть пример кода, где токен сохраняется, либо генерируется 2-й для одного и того же пользователя, но авторизованного с разных устройств?
lair
23.09.2018 21:15Опишите сценарий подробнее, я не понимаю, чего вы хотите добиться.
gerod
24.09.2018 15:15меня интересует пример использования OAuth 2, без использования собственных велосипедов в сценарии:
1 пользователь может авторизоваться с разных приложений(десктоп, плагин в браузере). Одновременная работа под одним акаунтом с разных приложений.
Я писал свой собственный метод авторизации и он мне не очень нравится, но пока работает — отлично.
В гайдах asp.net oauth 2 показана авторизация только с 1-го приложения. Если попытаться получить для другого приложения токен, то токен у первого становиться недействительным. Если есть где то гайд — буду благодарен за ссылку.
И да — я новичок в данной области поэтому просьба отнестись с пониманием.lair
24.09.2018 15:40+1Если попытаться получить для другого приложения токен, то токен у первого становиться недействительным.
Это очень странно. У разных приложений должен быть разный client_id; токены для разных client_id независимы. Более того, спецификация OAuth 2 не требует инвалидации токена и в рамках одного client_id (логично же: одно приложение может быть установлено на нескольких компьютерах); по умолчанию нормальные реализации просто выдают новый токен, не трогая старый.
Возьмите IdentityServer, он прикручивается в минимальное количество движений, в нем работает именно так.
Ascar Автор
23.09.2018 21:06Не думаю, что целесообразно делать это для единичного приложения. Если бы было несколько приложений в которых нужна такая сквозная авторизация то да. Вообщем изначально цель другая.
lair
23.09.2018 21:17Вообщем изначально цель другая.
Какая же? У вас в коде показан обычный такой Resource Owner Password Flow, им и пользуйтесь. Изобретать велосипеды в безопасности плохо.
x893
Сделали — хорошо, но это рутинный процесс, что SAML, что OAuth, что остальные. Полдня работы не повод писать заметку. Проще на github и посмотреть звёзды, последователей или орков.
Ernado
На минутку завис, пытаясь понять, когда орки успели добраться до гитхаба, и что они там делают :)
x893
:) ф выпала :)