Skip to content

Commit

Permalink
Added support for OAuth2
Browse files Browse the repository at this point in the history
Required members support
  • Loading branch information
Aragas committed May 26, 2024
1 parent 76ec096 commit 99e874f
Show file tree
Hide file tree
Showing 15 changed files with 206 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,7 +19,8 @@ public sealed class ButrNexusModsAuthHandler : AuthenticationHandler<ButrNexusMo
{
private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly ITokenBlacklistProvider _tokenBlacklistProvider;
private readonly INexusModsKeyValidator _nexusModsKeyValidator;
private readonly INexusModsApiKeyValidator _nexusModsApiKeyValidator;
private readonly INexusModsTokenValidator _nexusModsTokenValidator;
private readonly IHostEnvironment _environment;

public ButrNexusModsAuthHandler(
Expand All @@ -31,12 +31,14 @@ public ButrNexusModsAuthHandler(

IOptions<JsonSerializerOptions> jsonSerializerOptions,
ITokenBlacklistProvider tokenBlacklistProvider,
INexusModsKeyValidator nexusModsKeyValidator,
INexusModsApiKeyValidator nexusModsApiKeyValidator,
INexusModsTokenValidator nexusModsTokenValidator,
IHostEnvironment environment) : base(options, logger, encoder, clock)

Check warning on line 36 in src/BUTR.Authentication.NexusMods/Authentication/ButrNexusModsAuthHandler.cs

View workflow job for this annotation

GitHub Actions / Publish

'AuthenticationHandler<ButrNexusModsAuthSchemeOptions>.AuthenticationHandler(IOptionsMonitor<ButrNexusModsAuthSchemeOptions>, ILoggerFactory, UrlEncoder, ISystemClock)' is obsolete: 'ISystemClock is obsolete, use TimeProvider on AuthenticationSchemeOptions instead.'
{
_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));
}

Expand Down Expand Up @@ -95,15 +97,31 @@ protected override async Task<AuthenticateResult> 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[] {
Expand All @@ -113,7 +131,9 @@ protected override async Task<AuthenticateResult> 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)),

Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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() { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> 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<string, string> 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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> 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<string, string> Metadata { get; init; }
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
};
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>

<ImplicitUsings>false</ImplicitUsings>

Expand Down Expand Up @@ -42,10 +43,8 @@
</PropertyGroup>
<!--SorceLink-->
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BUTR.NexusMods.Shared\BUTR.NexusMods.Shared.csproj" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
<PackageReference Include="Required" Version="1.0.0" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,26 @@ public static AuthenticationBuilder AddNexusMods(this AuthenticationBuilder buil

public static IServiceCollection AddNexusModsDefaultServices(this IServiceCollection services)
{
services.Configure<ButrNexusModsKeyValidatorOptions>(opt =>
services.Configure<ButrNexusModsApiKeyValidatorOptions>(opt =>
{
opt.ApiEndpoint = "https://api.nexusmods.com/";
});
services.Configure<ButrNexusModsTokenValidatorOptions>(opt =>
{
opt.UsersEndpoint = "https://users.nexusmods.com/";
});
services.ConfigureOptions<RouteEncryptionKeyFromNexusModsAuthSchemeOptions>();

services.AddHttpClient<INexusModsKeyValidator, NexusModsKeyValidator>().ConfigureHttpClient((sp, client) =>
services.AddHttpClient<INexusModsApiKeyValidator, DefaultNexusModsApiKeyValidator>().ConfigureHttpClient((sp, client) =>
{
var opt = sp.GetRequiredService<IOptions<ButrNexusModsKeyValidatorOptions>>().Value;
var opt = sp.GetRequiredService<IOptions<ButrNexusModsApiKeyValidatorOptions>>().Value;
client.BaseAddress = new Uri(opt.ApiEndpoint);
});
services.AddHttpClient<INexusModsTokenValidator, DefaultNexusModsTokenValidator>().ConfigureHttpClient((sp, client) =>
{
var opt = sp.GetRequiredService<IOptions<ButrNexusModsTokenValidatorOptions>>().Value;
client.BaseAddress = new Uri(opt.UsersEndpoint);
});

services.AddScoped<ITokenGenerator, DefaultTokenGenerator>();
services.AddSingleton<ITokenBlacklistProvider, DefaultTokenBlacklistProvider>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace BUTR.Authentication.NexusMods.Options
{
public sealed record ButrNexusModsKeyValidatorOptions
public sealed record ButrNexusModsApiKeyValidatorOptions
{
public string ApiEndpoint { get; set; } = default!;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace BUTR.Authentication.NexusMods.Options;

public sealed record ButrNexusModsTokenValidatorOptions
{
public string UsersEndpoint { get; set; } = default!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@
namespace BUTR.Authentication.NexusMods.Services
{
/// <summary>
/// The default implementation of <see cref="INexusModsKeyValidator"/>. Uses <see cref="ButrNexusModsKeyValidatorOptions"/> for options.
/// The default implementation of <see cref="INexusModsApiKeyValidator"/>. Uses <see cref="ButrNexusModsApiKeyValidatorOptions"/> for options.
/// Needs special configuration. See the example below.
/// <example>
/// <code>
/// <para/>services.AddHttpClient&lt;INexusModsKeyValidator, NexusModsKeyValidator&gt;().ConfigureHttpClient((sp, client) =>
/// <para/>services.AddHttpClient&lt;INexusModsApiKeyValidator, NexusModsApiKeyValidator&gt;().ConfigureHttpClient((sp, client) =>
/// <para/>{
/// <para/> var opt = sp.GetRequiredService&lt;IOptions&lt;NexusModsKeyValidatorOptions&gt;&gt;().Value;
/// <para/> client.BaseAddress = new Uri(opt.Endpoint);
/// <para/> var opt = sp.GetRequiredService&lt;IOptions&lt;NexusModsApiKeyValidatorOptions&gt;&gt;().Value;
/// <para/> client.BaseAddress = new Uri(opt.ApiEndpoint);
/// <para/>});
/// </code>
/// </example>
/// </summary>
public sealed class NexusModsKeyValidator : INexusModsKeyValidator
public sealed class DefaultNexusModsApiKeyValidator : INexusModsApiKeyValidator
{
private sealed record NexusModsValidateResponse
{
Expand Down Expand Up @@ -61,13 +61,13 @@ private sealed record NexusModsValidateResponse
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonSerializerOptions;

public NexusModsKeyValidator(HttpClient httpClient, IOptions<JsonSerializerOptions> jsonSerializerOptions)
public DefaultNexusModsApiKeyValidator(HttpClient httpClient, IOptions<JsonSerializerOptions> jsonSerializerOptions)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_jsonSerializerOptions = jsonSerializerOptions.Value ?? throw new ArgumentNullException(nameof(jsonSerializerOptions));
}

public async Task<NexusModsUserInfo?> ValidateAPIKey(string apiKey)
public async Task<NexusModsUserInfo?> Validate(string apiKey)
{
try
{
Expand All @@ -90,6 +90,8 @@ public NexusModsKeyValidator(HttpClient httpClient, IOptions<JsonSerializerOptio
IsSupporter = responseType.IsSupporter,
IsPremium = responseType.IsPremium,
APIKey = responseType.Key,
AccessToken = null,
RefreshToken = null,
};
}
catch (Exception)
Expand Down
Loading

0 comments on commit 99e874f

Please sign in to comment.