diff --git a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/Login.graphql b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/Login.graphql index 1ed6c053ee..2d99ea6533 100644 --- a/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/Login.graphql +++ b/src/Tgstation.Server.Client.GraphQL/GQL/Mutations/Login.graphql @@ -1,6 +1,6 @@ mutation Login { login { - string + bearer errors { ... on ErrorMessageError { message diff --git a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs index bdb799c5e7..911cc7033e 100644 --- a/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/ILoginAuthority.cs @@ -1,8 +1,8 @@ using System.Threading; using System.Threading.Tasks; -using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Authority.Core; +using Tgstation.Server.Host.GraphQL.Mutations; namespace Tgstation.Server.Host.Authority { @@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Authority /// Attempt to login to the server with the current crentials. /// /// The for the operation. - /// A resulting in a . - ValueTask> AttemptLogin(CancellationToken cancellationToken); + /// A resulting in a and . + ValueTask> AttemptLogin(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs index 3ed1e1315d..8dbd4827a7 100644 --- a/src/Tgstation.Server.Host/Authority/LoginAuthority.cs +++ b/src/Tgstation.Server.Host/Authority/LoginAuthority.cs @@ -11,7 +11,9 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Authority.Core; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.GraphQL.Mutations; using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Models.Transformers; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Security.OAuth; using Tgstation.Server.Host.Utils; @@ -55,8 +57,8 @@ namespace Tgstation.Server.Host.Authority /// Generate an for a given . /// /// The to generate a response for. - /// A new, errored . - static AuthorityResponse GenerateHeadersExceptionResponse(HeadersException headersException) + /// A new, errored . + static AuthorityResponse GenerateHeadersExceptionResponse(HeadersException headersException) => new( new ErrorMessageResponse(ErrorCode.BadHeaders) { @@ -75,14 +77,6 @@ namespace Tgstation.Server.Host.Authority static async ValueTask SelectUserInfoFromQuery(IQueryable query, CancellationToken cancellationToken) { var users = await query - .Select(x => new User - { - Id = x.Id, - PasswordHash = x.PasswordHash, - Enabled = x.Enabled, - Name = x.Name, - SystemIdentifier = x.SystemIdentifier, - }) .ToListAsync(cancellationToken); // Pick the DB user first @@ -129,14 +123,14 @@ namespace Tgstation.Server.Host.Authority } /// - public async ValueTask> AttemptLogin(CancellationToken cancellationToken) + public async ValueTask> AttemptLogin(CancellationToken cancellationToken) { var headers = apiHeadersProvider.ApiHeaders; if (headers == null) return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!); if (headers.IsTokenAuthentication) - return BadRequest(ErrorCode.TokenWithToken); + return BadRequest(ErrorCode.TokenWithToken); var oAuthLogin = headers.OAuthProvider.HasValue; @@ -166,7 +160,7 @@ namespace Tgstation.Server.Host.Authority .GetValidator(oAuthProvider); if (validator == null) - return BadRequest(ErrorCode.OAuthProviderDisabled); + return BadRequest(ErrorCode.OAuthProviderDisabled); externalUserId = await validator .ValidateResponseCode(headers.OAuthCode!, cancellationToken); @@ -175,11 +169,11 @@ namespace Tgstation.Server.Host.Authority } catch (Octokit.RateLimitExceededException ex) { - return RateLimit(ex); + return RateLimit(ex); } if (externalUserId == null) - return Unauthorized(); + return Unauthorized(); query = query.Where( x => x.OAuthConnections!.Any( @@ -190,7 +184,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); @@ -202,7 +196,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 @@ -217,7 +211,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); @@ -260,16 +254,22 @@ 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 = tokenFactory.CreateToken(user, oAuthLogin); + var payload = new LoginPayload + { + Bearer = token, + User = ((IApiTransformable)user).ToApi(), + }; + if (usingSystemIdentity) - await CacheSystemIdentity(systemIdentity!, user, token); + await CacheSystemIdentity(systemIdentity!, user, payload); Logger.LogDebug("Successfully logged in user {userId}!", user.Id); - return new AuthorityResponse(token); + return new AuthorityResponse(payload); } } @@ -278,12 +278,12 @@ namespace Tgstation.Server.Host.Authority /// /// The to cache. /// The the was generated for. - /// The for the . + /// The for the successful login. /// A representing the running operation. - private async ValueTask CacheSystemIdentity(ISystemIdentity systemIdentity, User user, TokenResponse token) + private async ValueTask CacheSystemIdentity(ISystemIdentity systemIdentity, User user, LoginPayload loginPayload) { // expire the identity slightly after the auth token in case of lag - var identExpiry = token.ParseJwt().ValidTo; + var identExpiry = loginPayload.ToApi().ParseJwt().ValidTo; identExpiry += tokenFactory.ValidationParameters.ClockSkew; identExpiry += TimeSpan.FromSeconds(15); await identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry); diff --git a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs index 893357064b..7a90d23f17 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiRootController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiRootController.cs @@ -19,6 +19,7 @@ using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.GraphQL.Mutations; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.Security.OAuth; @@ -185,7 +186,7 @@ namespace Tgstation.Server.Host.Controllers return ValueTask.FromResult(HeadersIssue(ApiHeadersProvider.HeadersException!)); } - return loginAuthority.Invoke(this, authority => authority.AttemptLogin(cancellationToken)); + return loginAuthority.InvokeTransformable(this, authority => authority.AttemptLogin(cancellationToken)); } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Mutation.cs b/src/Tgstation.Server.Host/GraphQL/Mutation.cs index 31b896e4fc..f9b8d29d52 100644 --- a/src/Tgstation.Server.Host/GraphQL/Mutation.cs +++ b/src/Tgstation.Server.Host/GraphQL/Mutation.cs @@ -5,8 +5,8 @@ using System.Threading.Tasks; using HotChocolate; using HotChocolate.Types; -using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Authority; +using Tgstation.Server.Host.GraphQL.Mutations; namespace Tgstation.Server.Host.GraphQL { @@ -23,16 +23,14 @@ namespace Tgstation.Server.Host.GraphQL /// The for the operation. /// A Bearer token to be used with further communication with the server. [Error(typeof(ErrorMessageException))] - public async ValueTask Login( + public ValueTask Login( [Service] IGraphQLAuthorityInvoker loginAuthority, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(loginAuthority); - var tokenResponse = await loginAuthority.Invoke( - authority => authority.AttemptLogin(cancellationToken)); - - return tokenResponse!.Bearer!; + return loginAuthority.Invoke( + authority => authority.AttemptLogin(cancellationToken))!; } } } diff --git a/src/Tgstation.Server.Host/GraphQL/Mutations/LoginPayload.cs b/src/Tgstation.Server.Host/GraphQL/Mutations/LoginPayload.cs new file mode 100644 index 0000000000..31b9b2e0b3 --- /dev/null +++ b/src/Tgstation.Server.Host/GraphQL/Mutations/LoginPayload.cs @@ -0,0 +1,31 @@ +using HotChocolate; + +using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.GraphQL.Mutations +{ + /// + /// Success response for a login attempt. + /// + public sealed class LoginPayload : ILegacyApiTransformable + { + /// + /// The JSON Web Token (JWT) to use as a Bearer token for accessing the server. Contains an expiry time. + /// + public required string Bearer { get; init; } + + /// + /// The that was logged in. + /// + public required Types.User User { get; init; } + + /// + [GraphQLIgnore] + public TokenResponse ToApi() + => new() + { + Bearer = Bearer, + }; + } +} diff --git a/src/Tgstation.Server.Host/Security/ITokenFactory.cs b/src/Tgstation.Server.Host/Security/ITokenFactory.cs index 707c5d78ff..777122a2ed 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 OAuth login. - /// A new . - TokenResponse CreateToken(Models.User user, bool oAuth); + /// A new token . + string CreateToken(Models.User user, bool oAuth); } } diff --git a/src/Tgstation.Server.Host/Security/TokenFactory.cs b/src/Tgstation.Server.Host/Security/TokenFactory.cs index d2ab3c0527..9269472c87 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 TokenResponse CreateToken(User user, bool oAuth) + public string CreateToken(User user, bool oAuth) { ArgumentNullException.ThrowIfNull(user); @@ -139,10 +139,7 @@ namespace Tgstation.Server.Host.Security expiry.UtcDateTime, now.UtcDateTime)); - var tokenResponse = new TokenResponse - { - Bearer = tokenHandler.WriteToken(securityToken), - }; + var tokenResponse = tokenHandler.WriteToken(securityToken); return tokenResponse; } diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 8c8ee498c6..134257db9a 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -186,7 +186,6 @@ - diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs b/tests/Tgstation.Server.Host.Tests/Swarm/TestableSwarmNode.cs index 2fbc888b1b..c5f22614b8 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 TokenResponse CreateToken(User user, bool oAuth) + public string CreateToken(User user, bool oAuth) { throw new NotSupportedException(); } diff --git a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs index 5c320270e1..bac0433d4f 100644 --- a/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs +++ b/tests/Tgstation.Server.Tests/Live/RawRequestTests.cs @@ -470,7 +470,7 @@ namespace Tgstation.Server.Tests.Live }); Assert.IsNotNull(result.Data); - Assert.IsNull(result.Data.Login.String); + Assert.IsNull(result.Data.Login.Bearer); Assert.IsNotNull(result.Data.Login.Errors); Assert.AreEqual(1, result.Data.Login.Errors.Count); var castResult = result.Data.Login.Errors[0] is ILogin_Login_Errors_ErrorMessageError loginError;