139 lines
5.4 KiB
C#
139 lines
5.4 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using QuestionsApi.Models;
|
|
using QuestionsApi.Services;
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
using Xunit;
|
|
|
|
namespace QuestionsApi.Tests;
|
|
|
|
public sealed class AuthTests
|
|
{
|
|
[Theory]
|
|
[InlineData("valid_user", "[email protected]", "password1", null)]
|
|
[InlineData("ab", "[email protected]", "password1", "Username")]
|
|
[InlineData("invalid-user", "[email protected]", "password1", "Username")]
|
|
[InlineData("valid_user", "not-an-email", "password1", "email")]
|
|
[InlineData("valid_user", "[email protected]", "short", "Password")]
|
|
public void NewAccountValidationRejectsInvalidInput(
|
|
string username, string email, string password, string? expectedError)
|
|
{
|
|
var result = AuthValidation.ValidateNewAccount(username, email, password);
|
|
if (expectedError is null) Assert.Null(result);
|
|
else Assert.Contains(expectedError, result!, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public void PasswordResetCodesAreSixDigitsAndBoundToTheirRequest()
|
|
{
|
|
var service = new PasswordResetCodeService(Configuration(new Dictionary<string, string?>
|
|
{
|
|
["PasswordReset:HmacKey"] = "A-test-only-key-that-is-longer-than-thirty-two-bytes"
|
|
}));
|
|
var code = service.GenerateCode();
|
|
var hash = service.Hash("request-one", code);
|
|
|
|
Assert.Matches("^[0-9]{6}$", code);
|
|
Assert.True(service.Verify("request-one", code, hash));
|
|
Assert.False(service.Verify("request-two", code, hash));
|
|
Assert.False(service.Verify("request-one", "000000", hash));
|
|
}
|
|
|
|
[Fact]
|
|
public void PasswordResetCanRemainUnconfiguredWithoutBreakingAuthentication()
|
|
{
|
|
var service = new PasswordResetCodeService(Configuration([]));
|
|
Assert.False(service.IsConfigured);
|
|
}
|
|
|
|
[Fact]
|
|
public void JwtContainsCompatibleIdentityRoleAndAuthVersionClaims()
|
|
{
|
|
var service = new JwtTokenService(Configuration(new Dictionary<string, string?>
|
|
{
|
|
["Jwt:Key"] = "A-test-only-jwt-key-that-is-longer-than-thirty-two-bytes",
|
|
["Jwt:Issuer"] = "promiscuity",
|
|
["Jwt:Audience"] = "promiscuity-auth-api"
|
|
}));
|
|
var user = new AuthUser
|
|
{
|
|
Id = "507f1f77bcf86cd799439011",
|
|
Username = "doctor",
|
|
Role = "SUPER",
|
|
AuthVersion = 4
|
|
};
|
|
|
|
var generated = service.Generate(user);
|
|
var token = new JwtSecurityTokenHandler().ReadJwtToken(generated.Token);
|
|
|
|
Assert.Equal("promiscuity", token.Issuer);
|
|
Assert.Contains("promiscuity-auth-api", token.Audiences);
|
|
Assert.Equal("4", token.Claims.Single(claim => claim.Type == "auth_ver").Value);
|
|
Assert.Equal(generated.Jti, token.Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyUsersDefaultToAuthVersionZero()
|
|
{
|
|
Assert.Equal(0, new AuthUser().AuthVersion);
|
|
}
|
|
|
|
[Fact]
|
|
public void SteamClaimedIdExtractionRejectsUnexpectedHostsAndLengths()
|
|
{
|
|
Assert.Equal("76561198000000000",
|
|
SteamOpenIdService.ExtractSteamId("https://steamcommunity.com/openid/id/76561198000000000"));
|
|
Assert.Null(SteamOpenIdService.ExtractSteamId("https://example.com/openid/id/76561198000000000"));
|
|
Assert.Null(SteamOpenIdService.ExtractSteamId("https://steamcommunity.com/openid/id/123"));
|
|
}
|
|
|
|
[Fact]
|
|
public void GoogleProviderIsDisabledUntilAllSecureConfigurationExists()
|
|
{
|
|
var incomplete = new GoogleExternalAuthProvider(new TestHttpClientFactory(), Configuration(new Dictionary<string, string?>
|
|
{
|
|
["Google:ClientId"] = "client-id",
|
|
["Google:ReturnUrl"] = "https://example.test/api/Auth/external/google/callback"
|
|
}));
|
|
var complete = new GoogleExternalAuthProvider(new TestHttpClientFactory(), Configuration(new Dictionary<string, string?>
|
|
{
|
|
["Google:ClientId"] = "client-id",
|
|
["Google:ClientSecret"] = "client-secret",
|
|
["Google:ReturnUrl"] = "https://example.test/api/Auth/external/google/callback"
|
|
}));
|
|
|
|
Assert.False(incomplete.IsConfigured);
|
|
Assert.True(complete.IsConfigured);
|
|
}
|
|
|
|
[Fact]
|
|
public void GoogleAuthorizationUsesStateNonceAndPkce()
|
|
{
|
|
var provider = new GoogleExternalAuthProvider(new TestHttpClientFactory(), Configuration(new Dictionary<string, string?>
|
|
{
|
|
["Google:ClientId"] = "client-id",
|
|
["Google:ClientSecret"] = "client-secret",
|
|
["Google:ReturnUrl"] = "https://example.test/api/Auth/external/google/callback"
|
|
}));
|
|
var url = provider.BuildAuthorizationUrl(new ExternalAuthTransaction
|
|
{
|
|
Id = "transaction-state",
|
|
Nonce = "transaction-nonce",
|
|
CodeVerifier = "a-long-test-code-verifier"
|
|
});
|
|
|
|
Assert.Contains("state=transaction-state", url);
|
|
Assert.Contains("nonce=transaction-nonce", url);
|
|
Assert.Contains("code_challenge_method=S256", url);
|
|
Assert.Contains("scope=openid%20email%20profile", url);
|
|
Assert.DoesNotContain("client-secret", url);
|
|
}
|
|
|
|
private static IConfiguration Configuration(Dictionary<string, string?> values) =>
|
|
new ConfigurationBuilder().AddInMemoryCollection(values).Build();
|
|
|
|
private sealed class TestHttpClientFactory : IHttpClientFactory
|
|
{
|
|
public HttpClient CreateClient(string name) => new();
|
|
}
|
|
}
|