mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 15:07:03 +01:00
Improve LoginPayload
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
mutation Login {
|
||||
login {
|
||||
string
|
||||
bearer
|
||||
errors {
|
||||
... on ErrorMessageError {
|
||||
message
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="TokenResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<TokenResponse>> AttemptLogin(CancellationToken cancellationToken);
|
||||
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a <see cref="LoginPayload"/> and <see cref="Models.User"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
ValueTask<AuthorityResponse<LoginPayload>> AttemptLogin(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <see cref="AuthorityResponse{TResult}"/> for a given <paramref name="headersException"/>.
|
||||
/// </summary>
|
||||
/// <param name="headersException">The <see cref="HeadersException"/> to generate a response for.</param>
|
||||
/// <returns>A new, errored <see cref="TokenResponse"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
static AuthorityResponse<TokenResponse> GenerateHeadersExceptionResponse(HeadersException headersException)
|
||||
/// <returns>A new, errored <see cref="LoginPayload"/> <see cref="AuthorityResponse{TResult}"/>.</returns>
|
||||
static AuthorityResponse<LoginPayload> GenerateHeadersExceptionResponse(HeadersException headersException)
|
||||
=> new(
|
||||
new ErrorMessageResponse(ErrorCode.BadHeaders)
|
||||
{
|
||||
@@ -75,14 +77,6 @@ namespace Tgstation.Server.Host.Authority
|
||||
static async ValueTask<User?> SelectUserInfoFromQuery(IQueryable<User> 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
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<AuthorityResponse<TokenResponse>> AttemptLogin(CancellationToken cancellationToken)
|
||||
public async ValueTask<AuthorityResponse<LoginPayload>> AttemptLogin(CancellationToken cancellationToken)
|
||||
{
|
||||
var headers = apiHeadersProvider.ApiHeaders;
|
||||
if (headers == null)
|
||||
return GenerateHeadersExceptionResponse(apiHeadersProvider.HeadersException!);
|
||||
|
||||
if (headers.IsTokenAuthentication)
|
||||
return BadRequest<TokenResponse>(ErrorCode.TokenWithToken);
|
||||
return BadRequest<LoginPayload>(ErrorCode.TokenWithToken);
|
||||
|
||||
var oAuthLogin = headers.OAuthProvider.HasValue;
|
||||
|
||||
@@ -166,7 +160,7 @@ namespace Tgstation.Server.Host.Authority
|
||||
.GetValidator(oAuthProvider);
|
||||
|
||||
if (validator == null)
|
||||
return BadRequest<TokenResponse>(ErrorCode.OAuthProviderDisabled);
|
||||
return BadRequest<LoginPayload>(ErrorCode.OAuthProviderDisabled);
|
||||
|
||||
externalUserId = await validator
|
||||
.ValidateResponseCode(headers.OAuthCode!, cancellationToken);
|
||||
@@ -175,11 +169,11 @@ namespace Tgstation.Server.Host.Authority
|
||||
}
|
||||
catch (Octokit.RateLimitExceededException ex)
|
||||
{
|
||||
return RateLimit<TokenResponse>(ex);
|
||||
return RateLimit<LoginPayload>(ex);
|
||||
}
|
||||
|
||||
if (externalUserId == null)
|
||||
return Unauthorized<TokenResponse>();
|
||||
return Unauthorized<LoginPayload>();
|
||||
|
||||
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<TokenResponse>();
|
||||
return Unauthorized<LoginPayload>();
|
||||
|
||||
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<TokenResponse>();
|
||||
return Unauthorized<LoginPayload>();
|
||||
|
||||
// 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<TokenResponse>();
|
||||
return Unauthorized<LoginPayload>();
|
||||
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<TokenResponse>();
|
||||
return Forbid<LoginPayload>();
|
||||
}
|
||||
|
||||
var token = tokenFactory.CreateToken(user, oAuthLogin);
|
||||
var payload = new LoginPayload
|
||||
{
|
||||
Bearer = token,
|
||||
User = ((IApiTransformable<User, GraphQL.Types.User, UserGraphQLTransformer>)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<TokenResponse>(token);
|
||||
return new AuthorityResponse<LoginPayload>(payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,12 +278,12 @@ namespace Tgstation.Server.Host.Authority
|
||||
/// </summary>
|
||||
/// <param name="systemIdentity">The <see cref="ISystemIdentity"/> to cache.</param>
|
||||
/// <param name="user">The <see cref="User"/> the <paramref name="systemIdentity"/> was generated for.</param>
|
||||
/// <param name="token">The <see cref="TokenResponse"/> for the <paramref name="user"/>.</param>
|
||||
/// <param name="loginPayload">The <see cref="LoginPayload"/> for the successful login.</param>
|
||||
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
|
||||
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);
|
||||
|
||||
@@ -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<TokenResponse, TokenResponse>(this, authority => authority.AttemptLogin(cancellationToken));
|
||||
return loginAuthority.InvokeTransformable<LoginPayload, TokenResponse>(this, authority => authority.AttemptLogin(cancellationToken));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A Bearer token to be used with further communication with the server.</returns>
|
||||
[Error(typeof(ErrorMessageException))]
|
||||
public async ValueTask<string> Login(
|
||||
public ValueTask<LoginPayload> Login(
|
||||
[Service] IGraphQLAuthorityInvoker<ILoginAuthority> loginAuthority,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(loginAuthority);
|
||||
|
||||
var tokenResponse = await loginAuthority.Invoke<TokenResponse, TokenResponse>(
|
||||
authority => authority.AttemptLogin(cancellationToken));
|
||||
|
||||
return tokenResponse!.Bearer!;
|
||||
return loginAuthority.Invoke<LoginPayload, LoginPayload>(
|
||||
authority => authority.AttemptLogin(cancellationToken))!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using HotChocolate;
|
||||
|
||||
using Tgstation.Server.Api.Models.Response;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.GraphQL.Mutations
|
||||
{
|
||||
/// <summary>
|
||||
/// Success response for a login attempt.
|
||||
/// </summary>
|
||||
public sealed class LoginPayload : ILegacyApiTransformable<TokenResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// The JSON Web Token (JWT) to use as a Bearer token for accessing the server. Contains an expiry time.
|
||||
/// </summary>
|
||||
public required string Bearer { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="User"/> that was logged in.
|
||||
/// </summary>
|
||||
public required Types.User User { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[GraphQLIgnore]
|
||||
public TokenResponse ToApi()
|
||||
=> new()
|
||||
{
|
||||
Bearer = Bearer,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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="oAuth">Whether or not this is an OAuth login.</param>
|
||||
/// <returns>A new <see cref="TokenResponse"/>.</returns>
|
||||
TokenResponse CreateToken(Models.User user, bool oAuth);
|
||||
/// <returns>A new token <see cref="string"/>.</returns>
|
||||
string CreateToken(Models.User user, bool oAuth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace Tgstation.Server.Host.Security
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -186,7 +186,6 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="GraphQL\Mutations\" />
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user