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;