Added all necessary api endpoints for OpenID

This commit is contained in:
2024-12-22 10:55:24 +01:00
parent ba7584c771
commit 9b38a10797
15 changed files with 233 additions and 17 deletions

View File

@@ -24,6 +24,7 @@ public static class HopFrameAuthenticationExtensions {
service.AddScoped<ITokenContext, TokenContextImplementor>();
service.AddHttpClient();
service.AddMemoryCache();
service.AddScoped<IOpenIdAccessor, OpenIdAccessor>();
service.AddOptionsFromConfiguration<HopFrameAuthenticationOptions>(configuration);

View File

@@ -5,8 +5,8 @@ namespace HopFrame.Security.Authentication;
public class HopFrameAuthenticationOptions : OptionsFromConfiguration {
public override string Position { get; } = "HopFrame:Authentication";
public TimeSpan AccessTokenTime => AccessToken is null ? new(0, 0, 5, 0) : new(AccessToken.Days, AccessToken.Hours, AccessToken.Minutes, AccessToken.Seconds);
public TimeSpan RefreshTokenTime => RefreshToken is null ? new(30, 0, 0, 0) : new(RefreshToken.Days, RefreshToken.Hours, RefreshToken.Minutes, RefreshToken.Seconds);
public TimeSpan AccessTokenTime => AccessToken is null ? new(0, 0, 5, 0) : AccessToken.ConstructTimeSpan;
public TimeSpan RefreshTokenTime => RefreshToken is null ? new(30, 0, 0, 0) : RefreshToken.ConstructTimeSpan;
public TokenTime AccessToken { get; set; }
public TokenTime RefreshToken { get; set; }
@@ -16,5 +16,7 @@ public class HopFrameAuthenticationOptions : OptionsFromConfiguration {
public int Hours { get; set; }
public int Minutes { get; set; }
public int Seconds { get; set; }
public TimeSpan ConstructTimeSpan => new(Days, Hours, Minutes, Seconds);
}
}

View File

@@ -7,4 +7,5 @@ public interface IOpenIdAccessor {
Task<OpenIdToken> RequestToken(string code);
Task<string> ConstructAuthUri(string state = null);
Task<OpenIdIntrospection> InspectToken(string token);
Task<OpenIdToken> RefreshAccessToken(string refreshToken);
}

View File

@@ -1,12 +1,24 @@
using System.Text.Json;
using HopFrame.Security.Authentication.OpenID.Models;
using HopFrame.Security.Authentication.OpenID.Options;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace HopFrame.Security.Authentication.OpenID.Implementation;
internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options) : IOpenIdAccessor {
internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdOptions> options, IHttpContextAccessor accessor, IMemoryCache cache) : IOpenIdAccessor {
private const string DefaultCallbackEndpoint = "api/v1/openid/callback";
private const string ConfigurationCacheKey = "HopFrame:OpenID:Configuration";
private const string AuthCodeCacheKey = "HopFrame:OpenID:Code:";
private const string TokenCacheKey = "HopFrame:OpenID:Token:";
public async Task<OpenIdConfiguration> LoadConfiguration() {
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled && cache.TryGetValue(ConfigurationCacheKey, out object cachedConfiguration)) {
return cachedConfiguration as OpenIdConfiguration;
}
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);
@@ -14,10 +26,22 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
if (!response.IsSuccessStatusCode)
return null;
return await JsonSerializer.DeserializeAsync<OpenIdConfiguration>(await response.Content.ReadAsStreamAsync());
var config = await JsonSerializer.DeserializeAsync<OpenIdConfiguration>(await response.Content.ReadAsStreamAsync());
if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled)
cache.Set(ConfigurationCacheKey, config, options.Value.Cache.Configuration.TTL.ConstructTimeSpan);
return config;
}
public async Task<OpenIdToken> RequestToken(string code) {
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled && cache.TryGetValue(AuthCodeCacheKey + code, out object cachedToken)) {
return cachedToken as OpenIdToken;
}
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{DefaultCallbackEndpoint}";
var configuration = await LoadConfiguration();
var client = clientFactory.CreateClient();
@@ -25,7 +49,7 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
Content = new FormUrlEncodedContent(new Dictionary<string, string> {
{ "grant_type", "authorization_code" },
{ "code", code },
{ "redirect_uri", options.Value.Callback },
{ "redirect_uri", callback },
{ "client_id", options.Value.ClientId },
{ "client_secret", options.Value.ClientSecret }
})
@@ -35,15 +59,27 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
if (!response.IsSuccessStatusCode)
return null;
return await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
var token = await JsonSerializer.DeserializeAsync<OpenIdToken>(await response.Content.ReadAsStreamAsync());
if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled)
cache.Set(AuthCodeCacheKey + code, token, options.Value.Cache.Auth.TTL.ConstructTimeSpan);
return token;
}
public async Task<string> ConstructAuthUri(string state = null) {
var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http";
var callback = options.Value.Callback ?? $"{protocol}://{accessor.HttpContext!.Request.Host.Value}/{DefaultCallbackEndpoint}";
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}";
return $"{configuration.AuthorizationEndpoint}?response_type=code&client_id={options.Value.ClientId}&redirect_uri={callback}&scope=openid%20profile%20email%20offline_access&state={state}";
}
public async Task<OpenIdIntrospection> InspectToken(string token) {
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled && cache.TryGetValue(TokenCacheKey + token, out object cachedToken)) {
return cachedToken as OpenIdIntrospection;
}
var configuration = await LoadConfiguration();
var client = clientFactory.CreateClient();
@@ -59,6 +95,31 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions<OpenIdO
if (!response.IsSuccessStatusCode)
return null;
return await JsonSerializer.DeserializeAsync<OpenIdIntrospection>(await response.Content.ReadAsStreamAsync());
var introspection = await JsonSerializer.DeserializeAsync<OpenIdIntrospection>(await response.Content.ReadAsStreamAsync());
if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled)
cache.Set(TokenCacheKey + token, introspection, options.Value.Cache.Inspection.TTL.ConstructTimeSpan);
return introspection;
}
public async Task<OpenIdToken> RefreshAccessToken(string refreshToken) {
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", "refresh_token" },
{ "refresh_token", refreshToken },
{ "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());
}
}

View File

@@ -5,6 +5,9 @@ namespace HopFrame.Security.Authentication.OpenID.Models;
public sealed class OpenIdToken {
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
[JsonPropertyName("refresh_token")]
public string RefreshToken { get; set; }
[JsonPropertyName("token_type")]
public string TokenType { get; set; }

View File

@@ -12,4 +12,42 @@ public sealed class OpenIdOptions : OptionsFromConfiguration {
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string Callback { get; set; }
public HopFrameAuthenticationOptions.TokenTime RefreshToken { get; set; } = new() {
Days = 30
};
public CachingOptions Cache { get; set; } = new() {
Enabled = true,
Configuration = new() {
Enabled = true,
TTL = new() {
Minutes = 10
}
},
Auth = new() {
Enabled = true,
TTL = new() {
Seconds = 30
}
},
Inspection = new() {
Enabled = true,
TTL = new() {
Minutes = 2
}
}
};
public class CachingTypeOptions {
public bool Enabled { get; set; }
public HopFrameAuthenticationOptions.TokenTime TTL { get; set; }
}
public class CachingOptions {
public bool Enabled { get; set; }
public CachingTypeOptions Configuration { get; set; }
public CachingTypeOptions Auth { get; set; }
public CachingTypeOptions Inspection { get; set; }
}
}