2022-09-08 09:41:31 +00:00
|
|
|
|
using Newtonsoft.Json;
|
2022-09-08 12:38:32 +00:00
|
|
|
|
using VoidCat.Model;
|
2022-09-08 09:41:31 +00:00
|
|
|
|
using VoidCat.Model.User;
|
|
|
|
|
using VoidCat.Services.Abstractions;
|
|
|
|
|
|
|
|
|
|
namespace VoidCat.Services.Users.Auth;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Generic base class for OAuth2 code grant flow
|
|
|
|
|
/// </summary>
|
2022-09-08 12:38:32 +00:00
|
|
|
|
public abstract class GenericOAuth2Service : IOAuthProvider
|
2022-09-08 09:41:31 +00:00
|
|
|
|
{
|
2022-09-08 12:38:32 +00:00
|
|
|
|
private readonly Uri _uri;
|
2022-09-08 09:41:31 +00:00
|
|
|
|
private readonly HttpClient _client;
|
|
|
|
|
|
2022-09-08 12:38:32 +00:00
|
|
|
|
protected GenericOAuth2Service(HttpClient client, VoidSettings settings)
|
2022-09-08 09:41:31 +00:00
|
|
|
|
{
|
2022-09-08 12:38:32 +00:00
|
|
|
|
_uri = settings.SiteUrl;
|
2022-09-08 09:41:31 +00:00
|
|
|
|
_client = client;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public abstract string Id { get; }
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public Uri Authorize()
|
|
|
|
|
{
|
|
|
|
|
var ub = new UriBuilder(AuthorizeEndpoint)
|
|
|
|
|
{
|
|
|
|
|
Query = string.Join("&", BuildAuthorizeQuery().Select(a => $"{a.Key}={Uri.EscapeDataString(a.Value)}"))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return ub.Uri;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public async ValueTask<UserAuthToken> GetToken(string code)
|
|
|
|
|
{
|
|
|
|
|
var form = new FormUrlEncodedContent(BuildTokenQuery(code));
|
|
|
|
|
var rsp = await _client.PostAsync(TokenEndpoint, form);
|
|
|
|
|
var json = await rsp.Content.ReadAsStringAsync();
|
|
|
|
|
if (!rsp.IsSuccessStatusCode)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException($"Failed to get token from provider: {Id}, response: {json}");
|
|
|
|
|
}
|
2022-09-08 12:38:32 +00:00
|
|
|
|
|
|
|
|
|
var dto = JsonConvert.DeserializeObject<OAuthAccessToken>(json);
|
2022-09-08 09:41:31 +00:00
|
|
|
|
return TransformDto(dto!);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
public abstract ValueTask<InternalUser?> GetUserDetails(UserAuthToken token);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Build query args for authorize
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
2022-09-08 12:38:32 +00:00
|
|
|
|
protected virtual Dictionary<string, string> BuildAuthorizeQuery()
|
|
|
|
|
=> new()
|
|
|
|
|
{
|
|
|
|
|
{"response_type", "code"},
|
|
|
|
|
{"client_id", Details.ClientId!},
|
|
|
|
|
{"scope", string.Join(" ", Scopes)},
|
2022-09-08 13:29:31 +00:00
|
|
|
|
{"prompt", Prompt},
|
2022-09-08 12:38:32 +00:00
|
|
|
|
{"redirect_uri", new Uri(_uri, $"/auth/{Id}/token").ToString()}
|
|
|
|
|
};
|
2022-09-08 09:41:31 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Build query args for token generation
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <returns></returns>
|
2022-09-08 12:38:32 +00:00
|
|
|
|
protected virtual Dictionary<string, string> BuildTokenQuery(string code)
|
|
|
|
|
=> new()
|
|
|
|
|
{
|
|
|
|
|
{"client_id", Details.ClientId!},
|
|
|
|
|
{"client_secret", Details.ClientSecret!},
|
|
|
|
|
{"grant_type", "authorization_code"},
|
|
|
|
|
{"code", code},
|
|
|
|
|
{"redirect_uri", new Uri(_uri, $"/auth/{Id}/token").ToString()}
|
|
|
|
|
};
|
2022-09-08 09:41:31 +00:00
|
|
|
|
|
2022-09-08 13:29:31 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Prompt type for authorization
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected virtual string Prompt => "none";
|
|
|
|
|
|
2022-09-08 09:41:31 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Authorize url for this service
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected abstract Uri AuthorizeEndpoint { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Generate token url for this service
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected abstract Uri TokenEndpoint { get; }
|
2022-09-08 12:38:32 +00:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// OAuth client details
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected abstract OAuthDetails Details { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// OAuth scopes
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected abstract string[] Scopes { get; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Transform DTO to <see cref="UserAuthToken"/>
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="dto"></param>
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
protected virtual UserAuthToken TransformDto(OAuthAccessToken dto)
|
|
|
|
|
{
|
|
|
|
|
return new()
|
|
|
|
|
{
|
|
|
|
|
Id = Guid.NewGuid(),
|
|
|
|
|
Provider = Id,
|
|
|
|
|
AccessToken = dto.AccessToken,
|
|
|
|
|
Expires = DateTime.UtcNow.AddSeconds(dto.ExpiresIn),
|
|
|
|
|
TokenType = dto.TokenType,
|
|
|
|
|
RefreshToken = dto.RefreshToken,
|
2022-09-08 13:29:31 +00:00
|
|
|
|
Scope = dto.Scope,
|
|
|
|
|
IdToken = dto.IdToken
|
2022-09-08 12:38:32 +00:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected class OAuthAccessToken
|
|
|
|
|
{
|
2022-09-08 13:29:31 +00:00
|
|
|
|
[JsonProperty("access_token")]
|
|
|
|
|
public string AccessToken { get; init; }
|
2022-09-08 12:38:32 +00:00
|
|
|
|
|
2022-09-08 13:29:31 +00:00
|
|
|
|
[JsonProperty("expires_in")]
|
|
|
|
|
public int ExpiresIn { get; init; }
|
2022-09-08 12:38:32 +00:00
|
|
|
|
|
2022-09-08 13:29:31 +00:00
|
|
|
|
[JsonProperty("token_type")]
|
|
|
|
|
public string TokenType { get; init; }
|
2022-09-08 12:38:32 +00:00
|
|
|
|
|
2022-09-08 13:29:31 +00:00
|
|
|
|
[JsonProperty("refresh_token")]
|
|
|
|
|
public string RefreshToken { get; init; }
|
2022-09-08 12:38:32 +00:00
|
|
|
|
|
2022-09-08 13:29:31 +00:00
|
|
|
|
[JsonProperty("scope")]
|
|
|
|
|
public string Scope { get; init; }
|
|
|
|
|
|
|
|
|
|
[JsonProperty("id_token")]
|
|
|
|
|
public string IdToken { get; init; }
|
2022-09-08 12:38:32 +00:00
|
|
|
|
}
|
2022-09-08 09:41:31 +00:00
|
|
|
|
}
|