diff --git a/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsAuthHandler.cs b/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsAuthHandler.cs index c74a777..7c8277e 100644 --- a/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsAuthHandler.cs +++ b/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsAuthHandler.cs @@ -8,7 +8,6 @@ using Microsoft.Net.Http.Headers; using System; -using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; using System.Text.Json; @@ -20,7 +19,8 @@ public sealed class ButrNexusModsAuthHandler : AuthenticationHandler jsonSerializerOptions, ITokenBlacklistProvider tokenBlacklistProvider, - INexusModsKeyValidator nexusModsKeyValidator, + INexusModsApiKeyValidator nexusModsApiKeyValidator, + INexusModsTokenValidator nexusModsTokenValidator, IHostEnvironment environment) : base(options, logger, encoder, clock) { _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); _tokenBlacklistProvider = tokenBlacklistProvider ?? throw new ArgumentNullException(nameof(tokenBlacklistProvider)); - _nexusModsKeyValidator = nexusModsKeyValidator ?? throw new ArgumentNullException(nameof(nexusModsKeyValidator)); + _nexusModsApiKeyValidator = nexusModsApiKeyValidator ?? throw new ArgumentNullException(nameof(nexusModsApiKeyValidator)); + _nexusModsTokenValidator = nexusModsTokenValidator ?? throw new ArgumentNullException(nameof(nexusModsTokenValidator)); _environment = environment ?? throw new ArgumentNullException(nameof(environment)); } @@ -95,15 +97,31 @@ protected override async Task HandleAuthenticateAsync() { return AuthenticateResult.Fail("Token is blacklisted!"); } - - if (await _nexusModsKeyValidator.ValidateAPIKey(model.APIKey) is not { } validateResponse) + + if (!string.IsNullOrEmpty(model.APIKey)) { - return AuthenticateResult.Fail("Invalid NexusMods API Key!"); + if (await _nexusModsApiKeyValidator.Validate(model.APIKey) is not { } userInfo) + { + return AuthenticateResult.Fail("Invalid NexusMods API Key!"); + } + + if (!Compare(model, userInfo)) + { + return AuthenticateResult.Fail("NexusMods data has changed!"); + } } - if (!Compare(model, validateResponse)) + if (!string.IsNullOrEmpty(model.AccessToken)) { - return AuthenticateResult.Fail("NexusMods data has changed!"); + if (await _nexusModsTokenValidator.Validate(model.AccessToken, model.RefreshToken) is not { } userInfo) + { + return AuthenticateResult.Fail("Invalid NexusMods Access Token!"); + } + + if (!Compare(model, userInfo)) + { + return AuthenticateResult.Fail("NexusMods data has changed!"); + } } var claims = new[] { @@ -113,7 +131,9 @@ protected override async Task HandleAuthenticateAsync() new Claim(ButrNexusModsClaimTypes.ProfileUrl, model.ProfileUrl), new Claim(ButrNexusModsClaimTypes.IsSupporter, model.IsSupporter.ToString()), new Claim(ButrNexusModsClaimTypes.IsPremium, model.IsPremium.ToString()), - new Claim(ButrNexusModsClaimTypes.APIKey, model.APIKey), + new Claim(ButrNexusModsClaimTypes.APIKey, model.APIKey ?? ""), + new Claim(ButrNexusModsClaimTypes.AccessToken, model.AccessToken ?? ""), + new Claim(ButrNexusModsClaimTypes.RefreshToken, model.RefreshToken ?? ""), new Claim(ButrNexusModsClaimTypes.Role, model.Role), new Claim(ButrNexusModsClaimTypes.Metadata, JsonSerializer.Serialize(model.Metadata, _jsonSerializerOptions)), @@ -139,6 +159,10 @@ private static bool Compare(ButrNexusModsTokenData current, NexusModsUserInfo ne return false; if (current.APIKey != nexusModsData.APIKey) return false; + if (current.AccessToken != nexusModsData.AccessToken) + return false; + if (current.RefreshToken != nexusModsData.RefreshToken) + return false; return true; } } diff --git a/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsAuthSchemeOptions.cs b/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsAuthSchemeOptions.cs index cd033b9..376463f 100644 --- a/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsAuthSchemeOptions.cs +++ b/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsAuthSchemeOptions.cs @@ -1,9 +1,14 @@ using Microsoft.AspNetCore.Authentication; +using System.Diagnostics.CodeAnalysis; + namespace BUTR.Authentication.NexusMods.Authentication { public sealed class ButrNexusModsAuthSchemeOptions : AuthenticationSchemeOptions { - public string EncryptionKey { get; set; } = default!; + public required string EncryptionKey { get; set; } = default!; + + [SetsRequiredMembers] + public ButrNexusModsAuthSchemeOptions() { } } } \ No newline at end of file diff --git a/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsClaimTypes.cs b/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsClaimTypes.cs index a75f100..12e913b 100644 --- a/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsClaimTypes.cs +++ b/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsClaimTypes.cs @@ -9,6 +9,8 @@ public static class ButrNexusModsClaimTypes public const string IsSupporter = "NexusMods.IsSupporter"; public const string IsPremium = "NexusMods.IsPremium"; public const string APIKey = "NexusMods.APIKey"; + public const string AccessToken = "NexusMods.AccessToken"; + public const string RefreshToken = "NexusMods.RefreshToken"; public const string Role = "NexusMods.Role"; public const string Metadata = "NexusMods.Metadata"; diff --git a/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsTokenData.cs b/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsTokenData.cs index c65894f..74ed12d 100644 --- a/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsTokenData.cs +++ b/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsTokenData.cs @@ -5,17 +5,19 @@ namespace BUTR.Authentication.NexusMods.Authentication { public sealed record ButrNexusModsTokenData { - public ulong UserId { get; init; } = default!; - public string Name { get; init; } = default!; - public string EMail { get; init; } = default!; - public string ProfileUrl { get; init; } = default!; - public bool IsSupporter { get; init; } = default!; - public bool IsPremium { get; init; } = default!; - public string APIKey { get; init; } = default!; - public string Role { get; init; } = default!; - public Dictionary Metadata { get; init; } = default!; + public required ulong UserId { get; init; } + public required string Name { get; init; } + public required string EMail { get; init; } + public required string ProfileUrl { get; init; } + public required bool IsSupporter { get; init; } + public required bool IsPremium { get; init; } + public required string? APIKey { get; init; } + public required string? AccessToken { get; init; } + public required string? RefreshToken { get; init; } + public required string Role { get; init; } + public required Dictionary Metadata { get; init; } - public Guid TokenUid { get; init; } = default!; - public DateTime CreationTime { get; init; } = default!; + public required Guid TokenUid { get; init; } + public required DateTime CreationTime { get; init; } } } \ No newline at end of file diff --git a/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsUserInfo.cs b/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsUserInfo.cs index 786bcc0..2893980 100644 --- a/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsUserInfo.cs +++ b/src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsUserInfo.cs @@ -4,14 +4,16 @@ namespace BUTR.Authentication.NexusMods.Authentication { public sealed record ButrNexusModsUserInfo { - public uint UserId { get; init; } = default!; - public string Name { get; init; } = default!; - public string EMail { get; init; } = default!; - public string ProfileUrl { get; init; } = default!; - public bool IsSupporter { get; init; } = default!; - public bool IsPremium { get; init; } = default!; - public string APIKey { get; init; } = default!; - public string Role { get; init; } = default!; - public Dictionary Metadata { get; init; } = default!; + public required uint UserId { get; init; } + public required string Name { get; init; } + public required string EMail { get; init; } + public required string ProfileUrl { get; init; } + public required bool IsSupporter { get; init; } + public required bool IsPremium { get; init; } + public required string? APIKey { get; init; } + public required string? AccessToken { get; init; } + public required string? RefreshToken { get; init; } + public required string Role { get; init; } + public required Dictionary Metadata { get; init; } }; } \ No newline at end of file diff --git a/src/BUTR.Authentication.NexusMods/Authentication/NexusModsUserInfo.cs b/src/BUTR.Authentication.NexusMods/Authentication/NexusModsUserInfo.cs index 1a614b2..a9f78f9 100644 --- a/src/BUTR.Authentication.NexusMods/Authentication/NexusModsUserInfo.cs +++ b/src/BUTR.Authentication.NexusMods/Authentication/NexusModsUserInfo.cs @@ -2,12 +2,14 @@ { public sealed record NexusModsUserInfo { - public uint UserId { get; init; } = default!; - public string Name { get; init; } = default!; - public string EMail { get; init; } = default!; - public string ProfileUrl { get; init; } = default!; - public bool IsSupporter { get; init; } = default!; - public bool IsPremium { get; init; } = default!; - public string APIKey { get; init; } = default!; + public required uint UserId { get; init; } + public required string Name { get; init; } + public required string EMail { get; init; } + public required string ProfileUrl { get; init; } + public required bool IsSupporter { get; init; } + public required bool IsPremium { get; init; } + public required string? APIKey { get; init; } + public required string? AccessToken { get; init; } + public required string? RefreshToken { get; init; } }; } \ No newline at end of file diff --git a/src/BUTR.Authentication.NexusMods/BUTR.Authentication.NexusMods.csproj b/src/BUTR.Authentication.NexusMods/BUTR.Authentication.NexusMods.csproj index e9a09b7..01a4dee 100644 --- a/src/BUTR.Authentication.NexusMods/BUTR.Authentication.NexusMods.csproj +++ b/src/BUTR.Authentication.NexusMods/BUTR.Authentication.NexusMods.csproj @@ -1,8 +1,9 @@ - net6.0 + net6.0;net7.0;net8.0 enable + 12 false @@ -42,10 +43,8 @@ - - - - + + diff --git a/src/BUTR.Authentication.NexusMods/Extensions/ButrNexusModsExtensions.cs b/src/BUTR.Authentication.NexusMods/Extensions/ButrNexusModsExtensions.cs index f76d647..804efaa 100644 --- a/src/BUTR.Authentication.NexusMods/Extensions/ButrNexusModsExtensions.cs +++ b/src/BUTR.Authentication.NexusMods/Extensions/ButrNexusModsExtensions.cs @@ -17,17 +17,26 @@ public static AuthenticationBuilder AddNexusMods(this AuthenticationBuilder buil public static IServiceCollection AddNexusModsDefaultServices(this IServiceCollection services) { - services.Configure(opt => + services.Configure(opt => { opt.ApiEndpoint = "https://api.nexusmods.com/"; }); + services.Configure(opt => + { + opt.UsersEndpoint = "https://users.nexusmods.com/"; + }); services.ConfigureOptions(); - services.AddHttpClient().ConfigureHttpClient((sp, client) => + services.AddHttpClient().ConfigureHttpClient((sp, client) => { - var opt = sp.GetRequiredService>().Value; + var opt = sp.GetRequiredService>().Value; client.BaseAddress = new Uri(opt.ApiEndpoint); }); + services.AddHttpClient().ConfigureHttpClient((sp, client) => + { + var opt = sp.GetRequiredService>().Value; + client.BaseAddress = new Uri(opt.UsersEndpoint); + }); services.AddScoped(); services.AddSingleton(); diff --git a/src/BUTR.Authentication.NexusMods/Options/ButrNexusModsKeyValidatorOptions.cs b/src/BUTR.Authentication.NexusMods/Options/ButrNexusModsApiKeyValidatorOptions.cs similarity index 67% rename from src/BUTR.Authentication.NexusMods/Options/ButrNexusModsKeyValidatorOptions.cs rename to src/BUTR.Authentication.NexusMods/Options/ButrNexusModsApiKeyValidatorOptions.cs index ddf70a4..51da724 100644 --- a/src/BUTR.Authentication.NexusMods/Options/ButrNexusModsKeyValidatorOptions.cs +++ b/src/BUTR.Authentication.NexusMods/Options/ButrNexusModsApiKeyValidatorOptions.cs @@ -1,6 +1,6 @@ namespace BUTR.Authentication.NexusMods.Options { - public sealed record ButrNexusModsKeyValidatorOptions + public sealed record ButrNexusModsApiKeyValidatorOptions { public string ApiEndpoint { get; set; } = default!; } diff --git a/src/BUTR.Authentication.NexusMods/Options/ButrNexusModsTokenValidatorOptions.cs b/src/BUTR.Authentication.NexusMods/Options/ButrNexusModsTokenValidatorOptions.cs new file mode 100644 index 0000000..31c7257 --- /dev/null +++ b/src/BUTR.Authentication.NexusMods/Options/ButrNexusModsTokenValidatorOptions.cs @@ -0,0 +1,6 @@ +namespace BUTR.Authentication.NexusMods.Options; + +public sealed record ButrNexusModsTokenValidatorOptions +{ + public string UsersEndpoint { get; set; } = default!; +} \ No newline at end of file diff --git a/src/BUTR.Authentication.NexusMods/Services/NexusModsKeyValidator.cs b/src/BUTR.Authentication.NexusMods/Services/DefaultNexusModsApiKeyValidator.cs similarity index 81% rename from src/BUTR.Authentication.NexusMods/Services/NexusModsKeyValidator.cs rename to src/BUTR.Authentication.NexusMods/Services/DefaultNexusModsApiKeyValidator.cs index be03727..18a2e96 100644 --- a/src/BUTR.Authentication.NexusMods/Services/NexusModsKeyValidator.cs +++ b/src/BUTR.Authentication.NexusMods/Services/DefaultNexusModsApiKeyValidator.cs @@ -14,19 +14,19 @@ namespace BUTR.Authentication.NexusMods.Services { /// - /// The default implementation of . Uses for options. + /// The default implementation of . Uses for options. /// Needs special configuration. See the example below. /// /// - /// services.AddHttpClient<INexusModsKeyValidator, NexusModsKeyValidator>().ConfigureHttpClient((sp, client) => + /// services.AddHttpClient<INexusModsApiKeyValidator, NexusModsApiKeyValidator>().ConfigureHttpClient((sp, client) => /// { - /// var opt = sp.GetRequiredService<IOptions<NexusModsKeyValidatorOptions>>().Value; - /// client.BaseAddress = new Uri(opt.Endpoint); + /// var opt = sp.GetRequiredService<IOptions<NexusModsApiKeyValidatorOptions>>().Value; + /// client.BaseAddress = new Uri(opt.ApiEndpoint); /// }); /// /// /// - public sealed class NexusModsKeyValidator : INexusModsKeyValidator + public sealed class DefaultNexusModsApiKeyValidator : INexusModsApiKeyValidator { private sealed record NexusModsValidateResponse { @@ -61,13 +61,13 @@ private sealed record NexusModsValidateResponse private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonSerializerOptions; - public NexusModsKeyValidator(HttpClient httpClient, IOptions jsonSerializerOptions) + public DefaultNexusModsApiKeyValidator(HttpClient httpClient, IOptions jsonSerializerOptions) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); } - public async Task ValidateAPIKey(string apiKey) + public async Task Validate(string apiKey) { try { @@ -90,6 +90,8 @@ public NexusModsKeyValidator(HttpClient httpClient, IOptions + /// The default implementation of . Uses for options. + /// Needs special configuration. See the example below. + /// + /// + /// services.AddHttpClient<INexusModsTokenValidator, NexusModsTokenValidator>().ConfigureHttpClient((sp, client) => + /// { + /// var opt = sp.GetRequiredService<IOptions<NexusModsTokenValidatorOptions>>().Value; + /// client.BaseAddress = new Uri(opt.UsersEndpoint); + /// }); + /// + /// + /// + public sealed class DefaultNexusModsTokenValidator : INexusModsTokenValidator + { + public sealed record NexusModsUserInfoResponse( + [property: JsonPropertyName("sub")] string UserId, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("email")] string? Email, + [property: JsonPropertyName("membership_roles")] string[] MembershipRoles); + + private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonSerializerOptions; + + public DefaultNexusModsTokenValidator(HttpClient httpClient, IOptions jsonSerializerOptions) + { + _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(jsonSerializerOptions)); + } + + public async Task Validate(string accessToken, string refreshToken) + { + try + { + var request = new HttpRequestMessage(HttpMethod.Get, "oauth/userinfo"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + var response = await _httpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) + return null; + + var responseType = await response.Content.ReadFromJsonAsync(_jsonSerializerOptions); + if (responseType is null) + return null; + + return new() + { + UserId = uint.Parse(responseType.UserId), + Name = responseType.Name, + EMail = responseType.Email ?? "", + ProfileUrl = $"https://www.nexusmods.com/users/{responseType.UserId}", + IsSupporter = responseType.MembershipRoles.Contains("supporter"), + IsPremium = responseType.MembershipRoles.Contains("supporter"), + APIKey = null, + AccessToken = accessToken, + RefreshToken = refreshToken, + }; + } + catch (Exception) + { + return null; + } + } + } +} \ No newline at end of file diff --git a/src/BUTR.Authentication.NexusMods/Services/DefaultTokenGenerator.cs b/src/BUTR.Authentication.NexusMods/Services/DefaultTokenGenerator.cs index 00a591c..fc813bf 100644 --- a/src/BUTR.Authentication.NexusMods/Services/DefaultTokenGenerator.cs +++ b/src/BUTR.Authentication.NexusMods/Services/DefaultTokenGenerator.cs @@ -35,6 +35,8 @@ public Task GenerateTokenAsync(ButrNexusModsUserInfo userInfo) IsSupporter = userInfo.IsSupporter, IsPremium = userInfo.IsPremium, APIKey = userInfo.APIKey, + AccessToken = userInfo.AccessToken, + RefreshToken = userInfo.RefreshToken, Role = userInfo.Role, Metadata = userInfo.Metadata, diff --git a/src/BUTR.Authentication.NexusMods/Services/INexusModsKeyValidator.cs b/src/BUTR.Authentication.NexusMods/Services/INexusModsApiKeyValidator.cs similarity index 69% rename from src/BUTR.Authentication.NexusMods/Services/INexusModsKeyValidator.cs rename to src/BUTR.Authentication.NexusMods/Services/INexusModsApiKeyValidator.cs index bf19f79..c8766da 100644 --- a/src/BUTR.Authentication.NexusMods/Services/INexusModsKeyValidator.cs +++ b/src/BUTR.Authentication.NexusMods/Services/INexusModsApiKeyValidator.cs @@ -7,8 +7,8 @@ namespace BUTR.Authentication.NexusMods.Services /// /// Checks whether the API Key provided is valid. /// - public interface INexusModsKeyValidator + public interface INexusModsApiKeyValidator { - Task ValidateAPIKey(string apiKey); + Task Validate(string apiKey); } } \ No newline at end of file diff --git a/src/BUTR.Authentication.NexusMods/Services/INexusModsTokenValidator.cs b/src/BUTR.Authentication.NexusMods/Services/INexusModsTokenValidator.cs new file mode 100644 index 0000000..cc40000 --- /dev/null +++ b/src/BUTR.Authentication.NexusMods/Services/INexusModsTokenValidator.cs @@ -0,0 +1,13 @@ +using BUTR.Authentication.NexusMods.Authentication; + +using System.Threading.Tasks; + +namespace BUTR.Authentication.NexusMods.Services; + +/// +/// Checks whether the Bearer Token provided is valid. +/// +public interface INexusModsTokenValidator +{ + Task Validate(string accessToken, string? refreshToken); +} \ No newline at end of file