Added OpenID authentication method
This commit is contained in:
@@ -2,6 +2,8 @@ using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -19,7 +21,10 @@ public class HopFrameAuthentication(
|
||||
ISystemClock clock,
|
||||
ITokenRepository tokens,
|
||||
IPermissionRepository perms,
|
||||
IOptions<HopFrameAuthenticationOptions> tokenOptions)
|
||||
IOptions<HopFrameAuthenticationOptions> tokenOptions,
|
||||
IOptions<OpenIdOptions> openIdOptions,
|
||||
IUserRepository users,
|
||||
IOpenIdAccessor accessor)
|
||||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder, clock) {
|
||||
|
||||
public const string SchemeName = "HopFrame.Authentication";
|
||||
@@ -30,8 +35,39 @@ public class HopFrameAuthentication(
|
||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Headers["Token"];
|
||||
if (string.IsNullOrEmpty(accessToken)) accessToken = Request.Query["token"];
|
||||
if (string.IsNullOrEmpty(accessToken)) return AuthenticateResult.Fail("No Access Token provided");
|
||||
|
||||
|
||||
var tokenEntry = await tokens.GetToken(accessToken);
|
||||
|
||||
if (tokenEntry?.Type != Token.ApiTokenType && openIdOptions.Value.Enabled) {
|
||||
var result = await accessor.InspectToken(accessToken);
|
||||
|
||||
if (result is null || !result.Active)
|
||||
return AuthenticateResult.Fail("Invalid OpenID Connect token");
|
||||
|
||||
var email = result.Email;
|
||||
if (string.IsNullOrEmpty(email))
|
||||
return AuthenticateResult.Fail("OpenID user has no email associated to it");
|
||||
|
||||
var user = await users.GetUserByEmail(email);
|
||||
if (user is null) {
|
||||
if (!openIdOptions.Value.GenerateUsers)
|
||||
return AuthenticateResult.Fail("OpenID user does not exist");
|
||||
|
||||
var username = result.PreferredUsername;
|
||||
user = await users.AddUser(new User {
|
||||
Email = email,
|
||||
Username = username
|
||||
});
|
||||
}
|
||||
|
||||
var token = new Token {
|
||||
Owner = user,
|
||||
CreatedAt = DateTime.Now,
|
||||
Type = Token.OpenIdTokenType
|
||||
};
|
||||
var identity = await GenerateClaims(token);
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(identity, Scheme.Name));
|
||||
}
|
||||
|
||||
if (tokenEntry is null) return AuthenticateResult.Fail("The provided Access Token does not exist");
|
||||
|
||||
@@ -42,17 +78,22 @@ public class HopFrameAuthentication(
|
||||
if (tokenEntry.Owner is null)
|
||||
return AuthenticateResult.Fail("The provided Access Token does not match any user");
|
||||
|
||||
var principal = await GenerateClaims(tokenEntry);
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
||||
}
|
||||
|
||||
private async Task<ClaimsPrincipal> GenerateClaims(Token token) {
|
||||
var claims = new List<Claim> {
|
||||
new(HopFrameClaimTypes.AccessTokenId, accessToken),
|
||||
new(HopFrameClaimTypes.UserId, tokenEntry.Owner.Id.ToString())
|
||||
new(HopFrameClaimTypes.AccessTokenId, token.TokenId.ToString()),
|
||||
new(HopFrameClaimTypes.UserId, token.Owner.Id.ToString())
|
||||
};
|
||||
|
||||
var permissions = await perms.GetFullPermissions(tokenEntry);
|
||||
var permissions = await perms.GetFullPermissions(token);
|
||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||
|
||||
var principal = new ClaimsPrincipal();
|
||||
principal.AddIdentity(new ClaimsIdentity(claims, SchemeName));
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name));
|
||||
return principal;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
using HopFrame.Security.Authentication.OpenID;
|
||||
using HopFrame.Security.Authentication.OpenID.Implementation;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using HopFrame.Security.Authorization;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Options;
|
||||
@@ -20,8 +23,12 @@ public static class HopFrameAuthenticationExtensions {
|
||||
service.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
|
||||
service.AddScoped<ITokenContext, TokenContextImplementor>();
|
||||
|
||||
service.AddHttpClient();
|
||||
service.AddScoped<IOpenIdAccessor, OpenIdAccessor>();
|
||||
|
||||
service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);
|
||||
service.AddOptionsFromConfiguration<AdminPermissionOptions>(configuration);
|
||||
service.AddOptionsFromConfiguration<OpenIdOptions>(configuration);
|
||||
|
||||
service.AddAuthentication(HopFrameAuthentication.SchemeName).AddScheme<AuthenticationSchemeOptions, HopFrameAuthentication>(HopFrameAuthentication.SchemeName, _ => {});
|
||||
service.AddAuthorization();
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID;
|
||||
|
||||
public interface IOpenIdAccessor {
|
||||
Task<OpenIdConfiguration> LoadConfiguration();
|
||||
Task<OpenIdToken> RequestToken(string code);
|
||||
Task<string> ConstructAuthUri(string state = null);
|
||||
Task<OpenIdIntrospection> InspectToken(string token);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Text.Json;
|
||||
using HopFrame.Security.Authentication.OpenID.Models;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Implementation;
|
||||
|
||||
internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options) : IOpenIdAccessor {
|
||||
public async Task<OpenIdConfiguration> LoadConfiguration() {
|
||||
var client = clientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, Path.Combine(options.Value.Issuer, ".well-known/openid-configuration"));
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<OpenIdConfiguration>(await response.Content.ReadAsStreamAsync());
|
||||
}
|
||||
|
||||
public async Task<OpenIdToken> RequestToken(string code) {
|
||||
var configuration = await LoadConfiguration();
|
||||
|
||||
var client = clientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, configuration.TokenEndpoint) {
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||
{ "grant_type", "authorization_code" },
|
||||
{ "code", code },
|
||||
{ "redirect_uri", options.Value.Callback },
|
||||
{ "client_id", options.Value.ClientId },
|
||||
{ "client_secret", options.Value.ClientSecret }
|
||||
})
|
||||
};
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
|
||||
}
|
||||
|
||||
public async Task<string> ConstructAuthUri(string state = null) {
|
||||
var configuration = await LoadConfiguration();
|
||||
return $"{configuration.AuthorizationEndpoint}?response_type=code&client_id={options.Value.ClientId}&redirect_uri={options.Value.Callback}&scope=openid%20profile%20email&state={state}";
|
||||
}
|
||||
|
||||
public async Task<OpenIdIntrospection> InspectToken(string token) {
|
||||
var configuration = await LoadConfiguration();
|
||||
|
||||
var client = clientFactory.CreateClient();
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, configuration.IntrospectionEndpoint) {
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
|
||||
{ "token", token },
|
||||
{ "client_id", options.Value.ClientId },
|
||||
{ "client_secret", options.Value.ClientSecret }
|
||||
})
|
||||
};
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return null;
|
||||
|
||||
return await JsonSerializer.DeserializeAsync<OpenIdIntrospection>(await response.Content.ReadAsStreamAsync());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
public sealed class OpenIdConfiguration {
|
||||
[JsonPropertyName("issuer")]
|
||||
public string Issuer { get; set; }
|
||||
|
||||
[JsonPropertyName("authorization_endpoint")]
|
||||
public string AuthorizationEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("token_endpoint")]
|
||||
public string TokenEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("userinfo_endpoint")]
|
||||
public string UserinfoEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("end_session_endpoint")]
|
||||
public string EndSessionEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("introspection_endpoint")]
|
||||
public string IntrospectionEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("revocation_endpoint")]
|
||||
public string RevocationEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("device_authorization_endpoint")]
|
||||
public string DeviceAuthorizationEndpoint { get; set; }
|
||||
|
||||
[JsonPropertyName("response_types_supported")]
|
||||
public List<string> ResponseTypesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("response_modes_supported")]
|
||||
public List<string> ResponseModesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("jwks_uri")]
|
||||
public string JwksUri { get; set; }
|
||||
|
||||
[JsonPropertyName("grant_types_supported")]
|
||||
public List<string> GrantTypesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("id_token_signing_alg_values_supported")]
|
||||
public List<string> IdTokenSigningAlgValuesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("subject_types_supported")]
|
||||
public List<string> SubjectTypesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("token_endpoint_auth_methods_supported")]
|
||||
public List<string> TokenEndpointAuthMethodsSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("acr_values_supported")]
|
||||
public List<string> AcrValuesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("scopes_supported")]
|
||||
public List<string> ScopesSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("request_parameter_supported")]
|
||||
public bool RequestParameterSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("claims_supported")]
|
||||
public List<string> ClaimsSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("claims_parameter_supported")]
|
||||
public bool ClaimsParameterSupported { get; set; }
|
||||
|
||||
[JsonPropertyName("code_challenge_methods_supported")]
|
||||
public List<string> CodeChallengeMethodsSupported { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
public sealed class OpenIdIntrospection {
|
||||
[JsonPropertyName("iss")]
|
||||
public string Issuer { get; set; }
|
||||
|
||||
[JsonPropertyName("sub")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[JsonPropertyName("aud")]
|
||||
public string Audience { get; set; }
|
||||
|
||||
[JsonPropertyName("exp")]
|
||||
public long Expiration { get; set; }
|
||||
|
||||
[JsonPropertyName("iat")]
|
||||
public long IssuedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("auth_time")]
|
||||
public long AuthTime { get; set; }
|
||||
|
||||
[JsonPropertyName("acr")]
|
||||
public string Acr { get; set; }
|
||||
|
||||
[JsonPropertyName("amr")]
|
||||
public List<string> AuthenticationMethods { get; set; }
|
||||
|
||||
[JsonPropertyName("sid")]
|
||||
public string SessionId { get; set; }
|
||||
|
||||
[JsonPropertyName("email")]
|
||||
public string Email { get; set; }
|
||||
|
||||
[JsonPropertyName("email_verified")]
|
||||
public bool EmailVerified { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonPropertyName("given_name")]
|
||||
public string GivenName { get; set; }
|
||||
|
||||
[JsonPropertyName("preferred_username")]
|
||||
public string PreferredUsername { get; set; }
|
||||
|
||||
[JsonPropertyName("nickname")]
|
||||
public string Nickname { get; set; }
|
||||
|
||||
[JsonPropertyName("groups")]
|
||||
public List<string> Groups { get; set; }
|
||||
|
||||
[JsonPropertyName("active")]
|
||||
public bool Active { get; set; }
|
||||
|
||||
[JsonPropertyName("scope")]
|
||||
public string Scope { get; set; }
|
||||
|
||||
[JsonPropertyName("client_id")]
|
||||
public string ClientId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Models;
|
||||
|
||||
public sealed class OpenIdToken {
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[JsonPropertyName("token_type")]
|
||||
public string TokenType { get; set; }
|
||||
|
||||
[JsonPropertyName("expires_in")]
|
||||
public int ExpiresIn { get; set; }
|
||||
|
||||
[JsonPropertyName("id_token")]
|
||||
public string IdToken { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using HopFrame.Security.Options;
|
||||
|
||||
namespace HopFrame.Security.Authentication.OpenID.Options;
|
||||
|
||||
public sealed class OpenIdOptions : OptionsFromConfiguration {
|
||||
public override string Position { get; } = "HopFrame:Authentication:OpenID";
|
||||
|
||||
public bool Enabled { get; set; } = false;
|
||||
public bool GenerateUsers { get; set; } = true;
|
||||
|
||||
public string Issuer { get; set; }
|
||||
public string ClientId { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
public string Callback { get; set; }
|
||||
}
|
||||
@@ -21,6 +21,4 @@ public interface ITokenContext {
|
||||
/// The access token the user provided
|
||||
/// </summary>
|
||||
Token AccessToken { get; }
|
||||
|
||||
IList<string> ContextualPermissions { get; }
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
using HopFrame.Database.Models;
|
||||
using HopFrame.Database.Repositories;
|
||||
using HopFrame.Security.Authentication.OpenID.Options;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace HopFrame.Security.Claims;
|
||||
|
||||
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IPermissionRepository permissions) : ITokenContext {
|
||||
internal sealed class TokenContextImplementor(IHttpContextAccessor accessor, IUserRepository users, ITokenRepository tokens, IOptions<OpenIdOptions> options) : ITokenContext {
|
||||
public bool IsAuthenticated => !string.IsNullOrEmpty(accessor.HttpContext?.User.GetAccessTokenId());
|
||||
|
||||
public User User => users.GetUser(Guid.Parse(accessor.HttpContext?.User.GetUserId() ?? Guid.Empty.ToString())).GetAwaiter().GetResult();
|
||||
|
||||
public Token AccessToken => tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
|
||||
|
||||
public IList<string> ContextualPermissions => permissions.GetFullPermissions(AccessToken).GetAwaiter().GetResult();
|
||||
public Token AccessToken => options.Value.Enabled ? new Token {
|
||||
Owner = User,
|
||||
Type = Token.OpenIdTokenType,
|
||||
CreatedAt = DateTime.Now
|
||||
} : tokens.GetToken(accessor.HttpContext?.User.GetAccessTokenId()).GetAwaiter().GetResult();
|
||||
}
|
||||
Reference in New Issue
Block a user