diff --git a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs index 527cf41b85..c769ea6a4a 100644 --- a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs @@ -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. /// /// The for the operation. - /// A resulting in an authenticated token . - RequirementsGated> AttemptLogin(CancellationToken cancellationToken); + /// A resulting in a . + RequirementsGated> AttemptLogin(CancellationToken cancellationToken); /// /// Attempt to login to an OAuth service with the current OAuth credentials. diff --git a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs index f540ce6a4b..8f150845d4 100644 --- a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs @@ -139,7 +139,7 @@ namespace Tgstation.Server.Host.Authority } /// - public RequirementsGated> AttemptLogin(CancellationToken cancellationToken) + public RequirementsGated> AttemptLogin(CancellationToken cancellationToken) => new( () => null, () => AttemptLoginImpl(cancellationToken), @@ -176,19 +176,19 @@ namespace Tgstation.Server.Host.Authority /// Login process. /// /// The for the operation. - /// A resulting in the containing the authenticated bearer token. - private async ValueTask> AttemptLoginImpl(CancellationToken cancellationToken) + /// A resulting in the containing the . + private async ValueTask> AttemptLoginImpl(CancellationToken cancellationToken) { // password and oauth logins disabled if (securityConfigurationOptions.Value.OidcStrictMode) - return Unauthorized(); + return Unauthorized(); var headers = apiHeadersProvider.ApiHeaders; if (headers == null) - return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!); + return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!); if (headers.IsTokenAuthentication) - return BadRequest(ErrorCode.TokenWithToken); + return BadRequest(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(headers, oAuthProvider, true, cancellationToken); + var (errorResponse, oauthResult) = await TryOAuthenticate(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(); + return Unauthorized(); 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(); + return Unauthorized(); // 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(); + return Unauthorized(); 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(); + return Forbid(); } - 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(token); + return new AuthorityResponse(token); } } diff --git a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs index 0163af7bc3..97d171d7a6 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs @@ -195,7 +195,7 @@ namespace Tgstation.Server.Host.Controllers return ValueTask.FromResult(HeadersIssue(ApiHeadersProvider.HeadersException!)); } - return loginAuthority.InvokeTransformable(this, authority => authority.AttemptLogin(cancellationToken)); + return loginAuthority.InvokeTransformable(this, authority => authority.AttemptLogin(cancellationToken)); } /// diff --git a/src/Tgstation.Server.Host/Controllers/Transformers/TokenResponseTransformer.cs b/src/Tgstation.Server.Host/Controllers/Transformers/TokenResponseTransformer.cs index 7b7851100f..91ccf19147 100644 --- a/src/Tgstation.Server.Host/Controllers/Transformers/TokenResponseTransformer.cs +++ b/src/Tgstation.Server.Host/Controllers/Transformers/TokenResponseTransformer.cs @@ -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 { /// /// for s. /// - sealed class TokenResponseTransformer : TransformerBase + sealed class TokenResponseTransformer : TransformerBase { /// /// Initializes a new instance of the class. @@ -15,7 +16,7 @@ namespace Tgstation.Server.Host.Controllers.Transformers : base( token => new TokenResponse { - Bearer = token, + Bearer = token.Token, }) { } diff --git a/src/Tgstation.Server.Host/GraphQL/Mutation.cs b/src/Tgstation.Server.Host/GraphQL/Mutation.cs index cee0635398..613456ca02 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutation.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutation.cs @@ -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( + return loginAuthority.InvokeTransformable( authority => authority.AttemptLogin(cancellationToken)); } diff --git a/src/Tgstation.Server.Host/GraphQL/Transformers/LoginResultTransformer.cs b/src/Tgstation.Server.Host/GraphQL/Transformers/LoginResultTransformer.cs index 0c22ae2a63..4976d6696a 100644 --- a/src/Tgstation.Server.Host/GraphQL/Transformers/LoginResultTransformer.cs +++ b/src/Tgstation.Server.Host/GraphQL/Transformers/LoginResultTransformer.cs @@ -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 { /// /// for s. /// - sealed class LoginResultTransformer : TransformerBase + sealed class LoginResultTransformer : TransformerBase { /// /// Initializes a new instance of the class. @@ -15,7 +16,7 @@ namespace Tgstation.Server.Host.GraphQL.Transformers : base( token => new LoginResult { - Bearer = token, + Bearer = token.Token, }) { } diff --git a/src/Tgstation.Server.Host/Security/GeneratedToken.cs b/src/Tgstation.Server.Host/Security/GeneratedToken.cs new file mode 100644 index 0000000000..1d2cd08bcf --- /dev/null +++ b/src/Tgstation.Server.Host/Security/GeneratedToken.cs @@ -0,0 +1,20 @@ +using System; + +namespace Tgstation.Server.Host.Security +{ + /// + /// A securely generated token. + /// + public record struct GeneratedToken + { + /// + /// The token string. + /// + public required string Token { get; init; } + + /// + /// When the token expires. + /// + public required DateTimeOffset Expiry { get; init; } + } +} diff --git a/src/Tgstation.Server.Host/Security/ITokenFactory.cs b/src/Tgstation.Server.Host/Security/ITokenFactory.cs index 1baa29e6cc..70993bcf81 100644 --- a/src/Tgstation.Server.Host/Security/ITokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/ITokenFactory.cs @@ -26,7 +26,7 @@ namespace Tgstation.Server.Host.Security /// /// The to create the token for. Must have the field available. /// Whether or not this is an external service login. - /// A new token and the that it expires. - (string Token, DateTimeOffset Expiry) CreateToken(Models.User user, bool serviceLogin); + /// A new . + GeneratedToken CreateToken(Models.User user, bool serviceLogin); } } diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index d41803ef4d..e1d526e109 100644 --- a/src/Tgstation.Server.Host/Security/TokenFactory.cs +++ b/src/Tgstation.Server.Host/Security/TokenFactory.cs @@ -101,7 +101,7 @@ namespace Tgstation.Server.Host.Security } /// - 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, + }; } } } diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs index 60e465c342..6c7fd5f3d4 100644 --- a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs +++ b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs @@ -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(); }