Custom DTO for ITokenFactory

This commit is contained in:
Jordan Dominion
2025-08-17 15:07:11 -04:00
parent c0d94c0de3
commit 83ec6d0987
10 changed files with 55 additions and 27 deletions
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Tgstation.Server.Host.Authority.Core;
using Tgstation.Server.Host.GraphQL.Mutations.Payloads;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Authority
{
@@ -15,8 +16,8 @@ namespace Tgstation.Server.Host.Authority
/// Attempt to login to the server with the current Basic or OAuth credentials.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in an authenticated token <see cref="AuthorityResponse{TResult}"/>.</returns>
RequirementsGated<AuthorityResponse<string>> AttemptLogin(CancellationToken cancellationToken);
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="GeneratedToken"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
RequirementsGated<AuthorityResponse<GeneratedToken>> AttemptLogin(CancellationToken cancellationToken);
/// <summary>
/// Attempt to login to an OAuth service with the current OAuth credentials.
@@ -139,7 +139,7 @@ namespace Tgstation.Server.Host.Authority
}
/// <inheritdoc />
public RequirementsGated<AuthorityResponse<string>> AttemptLogin(CancellationToken cancellationToken)
public RequirementsGated<AuthorityResponse<GeneratedToken>> AttemptLogin(CancellationToken cancellationToken)
=> new(
() => null,
() => AttemptLoginImpl(cancellationToken),
@@ -176,19 +176,19 @@ namespace Tgstation.Server.Host.Authority
/// Login process.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/> containing the authenticated bearer token.</returns>
private async ValueTask<AuthorityResponse<string>> AttemptLoginImpl(CancellationToken cancellationToken)
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in the <see cref="AuthorityResponse{TResult}"/> containing the <see cref="GeneratedToken"/>.</returns>
private async ValueTask<AuthorityResponse<GeneratedToken>> AttemptLoginImpl(CancellationToken cancellationToken)
{
// password and oauth logins disabled
if (securityConfigurationOptions.Value.OidcStrictMode)
return Unauthorized<string>();
return Unauthorized<GeneratedToken>();
var headers = apiHeadersProvider.ApiHeaders;
if (headers == null)
return GenerateHeadersExceptionResponse<string>(apiHeadersProvider.HeadersException!);
return GenerateHeadersExceptionResponse<GeneratedToken>(apiHeadersProvider.HeadersException!);
if (headers.IsTokenAuthentication)
return BadRequest<string>(ErrorCode.TokenWithToken);
return BadRequest<GeneratedToken>(ErrorCode.TokenWithToken);
var oAuthLogin = headers.OAuthProvider.HasValue;
@@ -211,7 +211,7 @@ namespace Tgstation.Server.Host.Authority
if (oAuthLogin)
{
var oAuthProvider = headers.OAuthProvider!.Value;
var (errorResponse, oauthResult) = await TryOAuthenticate<string>(headers, oAuthProvider, true, cancellationToken);
var (errorResponse, oauthResult) = await TryOAuthenticate<GeneratedToken>(headers, oAuthProvider, true, cancellationToken);
if (errorResponse != null)
return errorResponse;
@@ -224,7 +224,7 @@ namespace Tgstation.Server.Host.Authority
{
var canonicalUserName = User.CanonicalizeName(headers.Username!);
if (canonicalUserName == User.CanonicalizeName(User.TgsSystemUserName))
return Unauthorized<string>();
return Unauthorized<GeneratedToken>();
if (systemIdentity == null)
query = query.Where(x => x.CanonicalName == canonicalUserName);
@@ -236,7 +236,7 @@ namespace Tgstation.Server.Host.Authority
// No user? You're not allowed
if (user == null)
return Unauthorized<string>();
return Unauthorized<GeneratedToken>();
// A system user may have had their name AND password changed to one in our DB...
// Or a DB user was created that had the same user/pass as a system user
@@ -251,7 +251,7 @@ namespace Tgstation.Server.Host.Authority
{
// DB User password check and update
if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, headers.Password!))
return Unauthorized<string>();
return Unauthorized<GeneratedToken>();
if (user.PasswordHash != originalHash)
{
Logger.LogDebug("User ID {userId}'s password hash needs a refresh, updating database.", user.Id);
@@ -294,17 +294,17 @@ namespace Tgstation.Server.Host.Authority
if (!user.Enabled!.Value)
{
Logger.LogTrace("Not logging in disabled user {userId}.", user.Id);
return Forbid<string>();
return Forbid<GeneratedToken>();
}
var (token, expiresAt) = tokenFactory.CreateToken(user, oAuthLogin);
var token = tokenFactory.CreateToken(user, oAuthLogin);
if (usingSystemIdentity)
await CacheSystemIdentity(systemIdentity!, user, expiresAt);
await CacheSystemIdentity(systemIdentity!, user, token.Expiry);
Logger.LogDebug("Successfully logged in user {userId}!", user.Id);
return new AuthorityResponse<string>(token);
return new AuthorityResponse<GeneratedToken>(token);
}
}
@@ -195,7 +195,7 @@ namespace Tgstation.Server.Host.Controllers
return ValueTask.FromResult(HeadersIssue(ApiHeadersProvider.HeadersException!));
}
return loginAuthority.InvokeTransformable<string, TokenResponse, TokenResponseTransformer>(this, authority => authority.AttemptLogin(cancellationToken));
return loginAuthority.InvokeTransformable<GeneratedToken, TokenResponse, TokenResponseTransformer>(this, authority => authority.AttemptLogin(cancellationToken));
}
/// <summary>
@@ -1,12 +1,13 @@
using Tgstation.Server.Api.Models.Response;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Controllers.Transformers
{
/// <summary>
/// <see cref="ITransformer{TInput, TOutput}"/> for <see cref="TokenResponse"/>s.
/// </summary>
sealed class TokenResponseTransformer : TransformerBase<string, TokenResponse>
sealed class TokenResponseTransformer : TransformerBase<GeneratedToken, TokenResponse>
{
/// <summary>
/// Initializes a new instance of the <see cref="TokenResponseTransformer"/> class.
@@ -15,7 +16,7 @@ namespace Tgstation.Server.Host.Controllers.Transformers
: base(
token => new TokenResponse
{
Bearer = token,
Bearer = token.Token,
})
{
}
@@ -9,6 +9,7 @@ using HotChocolate.Types;
using Tgstation.Server.Host.Authority;
using Tgstation.Server.Host.GraphQL.Mutations.Payloads;
using Tgstation.Server.Host.GraphQL.Transformers;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.GraphQL
{
@@ -39,7 +40,7 @@ namespace Tgstation.Server.Host.GraphQL
{
ArgumentNullException.ThrowIfNull(loginAuthority);
return loginAuthority.InvokeTransformable<string, LoginResult, LoginResultTransformer>(
return loginAuthority.InvokeTransformable<GeneratedToken, LoginResult, LoginResultTransformer>(
authority => authority.AttemptLogin(cancellationToken));
}
@@ -1,12 +1,13 @@
using Tgstation.Server.Host.GraphQL.Mutations.Payloads;
using Tgstation.Server.Host.Models;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.GraphQL.Transformers
{
/// <summary>
/// <see cref="ITransformer{TInput, TOutput}"/> for <see cref="LoginResult"/>s.
/// </summary>
sealed class LoginResultTransformer : TransformerBase<string, LoginResult>
sealed class LoginResultTransformer : TransformerBase<GeneratedToken, LoginResult>
{
/// <summary>
/// Initializes a new instance of the <see cref="LoginResultTransformer"/> class.
@@ -15,7 +16,7 @@ namespace Tgstation.Server.Host.GraphQL.Transformers
: base(
token => new LoginResult
{
Bearer = token,
Bearer = token.Token,
})
{
}
@@ -0,0 +1,20 @@
using System;
namespace Tgstation.Server.Host.Security
{
/// <summary>
/// A securely generated token.
/// </summary>
public record struct GeneratedToken
{
/// <summary>
/// The token string.
/// </summary>
public required string Token { get; init; }
/// <summary>
/// When the token expires.
/// </summary>
public required DateTimeOffset Expiry { get; init; }
}
}
@@ -26,7 +26,7 @@ namespace Tgstation.Server.Host.Security
/// </summary>
/// <param name="user">The <see cref="Models.User"/> to create the token for. Must have the <see cref="Api.Models.EntityId.Id"/> field available.</param>
/// <param name="serviceLogin">Whether or not this is an external service login.</param>
/// <returns>A new token <see cref="string"/> and the <see cref="DateTimeOffset"/> that it expires.</returns>
(string Token, DateTimeOffset Expiry) CreateToken(Models.User user, bool serviceLogin);
/// <returns>A new <see cref="GeneratedToken"/>.</returns>
GeneratedToken CreateToken(Models.User user, bool serviceLogin);
}
}
@@ -101,7 +101,7 @@ namespace Tgstation.Server.Host.Security
}
/// <inheritdoc />
public (string Token, DateTimeOffset Expiry) CreateToken(User user, bool serviceLogin)
public GeneratedToken CreateToken(User user, bool serviceLogin)
{
ArgumentNullException.ThrowIfNull(user);
@@ -141,7 +141,11 @@ namespace Tgstation.Server.Host.Security
var tokenResponse = tokenHandler.WriteToken(securityToken);
return (Token: tokenResponse, Expiry: expiry);
return new GeneratedToken
{
Token = tokenResponse,
Expiry = expiry,
};
}
}
}
@@ -89,7 +89,7 @@ namespace Tgstation.Server.Host.Swarm.Tests
public TokenValidationParameters ValidationParameters => throw new NotSupportedException();
public (string, DateTimeOffset) CreateToken(User user, bool serviceLogin)
public GeneratedToken CreateToken(User user, bool serviceLogin)
{
throw new NotSupportedException();
}