tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
LoginAuthority.cs
Go to the documentation of this file.
1using System;
2using System.Linq;
3using System.Threading;
4using System.Threading.Tasks;
5
6using Microsoft.EntityFrameworkCore;
7using Microsoft.Extensions.Logging;
8
20
22{
25 {
30
35
40
45
50
55
60
68 => new(
69 new ErrorMessageResponse(ErrorCode.BadHeaders)
70 {
71 AdditionalData = headersException.Message,
72 },
73 headersException.ParseErrors.HasFlag(HeaderErrorTypes.Accept)
74 ? HttpFailureResponse.NotAcceptable
75 : HttpFailureResponse.BadRequest);
76
83 static async ValueTask<User?> SelectUserInfoFromQuery(IQueryable<User> query, CancellationToken cancellationToken)
84 {
85 var users = await query
86 .ToListAsync(cancellationToken);
87
88 // Pick the DB user first
89 var user = users
90 .OrderByDescending(dbUser => dbUser.SystemIdentifier == null)
91 .FirstOrDefault();
92
93 return user;
94 }
95
110 IAuthenticationContext authenticationContext,
111 IDatabaseContext databaseContext,
112 ILogger<LoginAuthority> logger,
120 : base(
121 authenticationContext,
122 databaseContext,
123 logger)
124 {
125 this.apiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider));
126 this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
127 this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders));
128 this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
129 this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
130 this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
131 this.sessionInvalidationTracker = sessionInvalidationTracker ?? throw new ArgumentNullException(nameof(sessionInvalidationTracker));
132 }
133
135 public async ValueTask<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken)
136 {
137 var headers = apiHeadersProvider.ApiHeaders;
138 if (headers == null)
139 return GenerateHeadersExceptionResponse<LoginResult>(apiHeadersProvider.HeadersException!);
140
141 if (headers.IsTokenAuthentication)
142 return BadRequest<LoginResult>(ErrorCode.TokenWithToken);
143
144 var oAuthLogin = headers.OAuthProvider.HasValue;
145
146 ISystemIdentity? systemIdentity = null;
147 if (!oAuthLogin)
148 try
149 {
150 // trust the system over the database because a user's name can change while still having the same SID
151 systemIdentity = await systemIdentityFactory.CreateSystemIdentity(headers.Username!, headers.Password!, cancellationToken);
152 }
153 catch (NotImplementedException)
154 {
155 // Intentionally suppressed
156 }
157
158 using (systemIdentity)
159 {
160 // Get the user from the database
161 IQueryable<User> query = DatabaseContext.Users.AsQueryable();
162 if (oAuthLogin)
163 {
164 var oAuthProvider = headers.OAuthProvider!.Value;
165 var (errorResponse, oauthResult) = await TryOAuthenticate<LoginResult>(headers, oAuthProvider, true, cancellationToken);
166 if (errorResponse != null)
167 return errorResponse;
168
169 query = query.Where(
170 x => x.OAuthConnections!.Any(
171 y => y.Provider == oAuthProvider
172 && y.ExternalUserId == oauthResult!.Value.UserID));
173 }
174 else
175 {
176 var canonicalUserName = User.CanonicalizeName(headers.Username!);
177 if (canonicalUserName == User.CanonicalizeName(User.TgsSystemUserName))
178 return Unauthorized<LoginResult>();
179
180 if (systemIdentity == null)
181 query = query.Where(x => x.CanonicalName == canonicalUserName);
182 else
183 query = query.Where(x => x.CanonicalName == canonicalUserName || x.SystemIdentifier == systemIdentity.Uid);
184 }
185
186 var user = await SelectUserInfoFromQuery(query, cancellationToken);
187
188 // No user? You're not allowed
189 if (user == null)
190 return Unauthorized<LoginResult>();
191
192 // A system user may have had their name AND password changed to one in our DB...
193 // Or a DB user was created that had the same user/pass as a system user
194 // Dumb admins...
195 // FALLBACK TO THE DB USER HERE, DO NOT REVEAL A SYSTEM LOGIN!!!
196 // This of course, allows system users to discover TGS users in this (HIGHLY IMPROBABLE) case but that is not our fault
197 var originalHash = user.PasswordHash;
198 var isLikelyDbUser = originalHash != null;
199 var usingSystemIdentity = systemIdentity != null && !isLikelyDbUser;
200 if (!oAuthLogin)
201 if (!usingSystemIdentity)
202 {
203 // DB User password check and update
204 if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, headers.Password!))
205 return Unauthorized<LoginResult>();
206 if (user.PasswordHash != originalHash)
207 {
208 Logger.LogDebug("User ID {userId}'s password hash needs a refresh, updating database.", user.Id);
209 var updatedUser = new User
210 {
211 Id = user.Id,
212 };
213 DatabaseContext.Users.Attach(updatedUser);
214 updatedUser.PasswordHash = user.PasswordHash;
215 await DatabaseContext.Save(cancellationToken);
216 }
217 }
218 else
219 {
220 var usernameMismatch = systemIdentity!.Username != user.Name;
221 if (isLikelyDbUser || usernameMismatch)
222 {
223 DatabaseContext.Users.Attach(user);
224 if (usernameMismatch)
225 {
226 // System identity username change update
227 Logger.LogDebug("User ID {userId}'s system identity needs a refresh, updating database.", user.Id);
228 user.Name = systemIdentity.Username;
229 user.CanonicalName = User.CanonicalizeName(user.Name);
230 }
231
232 if (isLikelyDbUser)
233 {
234 // cleanup from https://github.com/tgstation/tgstation-server/issues/1528
235 Logger.LogDebug("System user ID {userId}'s PasswordHash is polluted, updating database.", user.Id);
236 user.PasswordHash = null;
238 }
239
240 await DatabaseContext.Save(cancellationToken);
241 }
242 }
243
244 // Now that the bookeeping is done, tell them to fuck off if necessary
245 if (!user.Enabled!.Value)
246 {
247 Logger.LogTrace("Not logging in disabled user {userId}.", user.Id);
248 return Forbid<LoginResult>();
249 }
250
251 var token = tokenFactory.CreateToken(user, oAuthLogin);
252 var payload = new LoginResult
253 {
254 Bearer = token,
255 User = ((IApiTransformable<User, GraphQL.Types.User, UserGraphQLTransformer>)user).ToApi(),
256 };
257
258 if (usingSystemIdentity)
259 await CacheSystemIdentity(systemIdentity!, user, payload);
260
261 Logger.LogDebug("Successfully logged in user {userId}!", user.Id);
262
263 return new AuthorityResponse<LoginResult>(payload);
264 }
265 }
266
268 public async ValueTask<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
269 {
270 var headers = apiHeadersProvider.ApiHeaders;
271 if (headers == null)
272 return GenerateHeadersExceptionResponse<OAuthGatewayLoginResult>(apiHeadersProvider.HeadersException!);
273
274 var oAuthProvider = headers.OAuthProvider;
275 if (!oAuthProvider.HasValue)
276 return BadRequest<OAuthGatewayLoginResult>(ErrorCode.BadHeaders);
277
278 var (errorResponse, oAuthResult) = await TryOAuthenticate<OAuthGatewayLoginResult>(headers, oAuthProvider.Value, false, cancellationToken);
279 if (errorResponse != null)
280 return errorResponse;
281
282 Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value);
283
286 {
287 AccessCode = oAuthResult!.Value.AccessCode,
288 });
289 }
290
298 private async ValueTask CacheSystemIdentity(ISystemIdentity systemIdentity, User user, LoginResult loginPayload)
299 {
300 // expire the identity slightly after the auth token in case of lag
301 var identExpiry = loginPayload.ToApi().ParseJwt().ValidTo;
302 identExpiry += tokenFactory.ValidationParameters.ClockSkew;
303 identExpiry += TimeSpan.FromSeconds(15);
304 await identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry);
305 }
306
316 async ValueTask<(AuthorityResponse<TResult>? ErrorResponse, (string? UserID, string AccessCode)? OAuthResult)> TryOAuthenticate<TResult>(ApiHeaders headers, OAuthProvider oAuthProvider, bool forLogin, CancellationToken cancellationToken)
317 {
318 (string? UserID, string AccessCode)? oauthResult;
319 try
320 {
321 var validator = oAuthProviders
322 .GetValidator(oAuthProvider, forLogin);
323
324 if (validator == null)
325 return (BadRequest<TResult>(ErrorCode.OAuthProviderDisabled), null);
326 oauthResult = await validator
327 .ValidateResponseCode(headers.OAuthCode!, forLogin, cancellationToken);
328
329 Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, oauthResult);
330 }
331 catch (Octokit.RateLimitExceededException ex)
332 {
333 return (RateLimit<TResult>(ex), null);
334 }
335
336 if (!oauthResult.HasValue)
337 return (Unauthorized<TResult>(), null);
338
339 return (null, OAuthResult: oauthResult);
340 }
341 }
342}
Represents the header that must be present for every server request.
Definition ApiHeaders.cs:25
string? OAuthCode
The OAuth code in use.
Thrown when trying to generate ApiHeaders from Microsoft.AspNetCore.Http.Headers.RequestHeaders fails...
virtual ? long Id
The ID of the entity.
Definition EntityId.cs:13
Represents an error message returned by the server.
string? Message
A human readable description of the error.
JsonWebToken ParseJwt()
Parses the Bearer as a JsonWebToken.
static AuthorityResponse< TResult > Unauthorized< TResult >()
Generates a HttpFailureResponse.Unauthorized type AuthorityResponse<TResult>.
AuthorityResponse< TResult > RateLimit< TResult >(RateLimitExceededException rateLimitException)
Generates a HttpFailureResponse.RateLimited type AuthorityResponse.
ILogger< AuthorityBase > Logger
Gets the ILogger for the AuthorityBase.
static AuthorityResponse< TResult > BadRequest< TResult >(ErrorCode errorCode)
Generates a HttpFailureResponse.BadRequest type AuthorityResponse<TResult>.
readonly IApiHeadersProvider apiHeadersProvider
The IApiHeadersProvider for the LoginAuthority.
async ValueTask< AuthorityResponse< LoginResult > > AttemptLogin(CancellationToken cancellationToken)
Attempt to login to the server with the current Basic or OAuth credentials.A ValueTask<TResult> resul...
readonly IOAuthProviders oAuthProviders
The IOAuthProviders for the LoginAuthority.
async ValueTask CacheSystemIdentity(ISystemIdentity systemIdentity, User user, LoginResult loginPayload)
Add a given systemIdentity to the identityCache.
readonly IIdentityCache identityCache
The IIdentityCache for the LoginAuthority.
async ValueTask<(AuthorityResponse< TResult >? ErrorResponse,(string? UserID, string AccessCode)? OAuthResult)> TryOAuthenticate< TResult >(ApiHeaders headers, OAuthProvider oAuthProvider, bool forLogin, CancellationToken cancellationToken)
Attempt OAuth authentication.
LoginAuthority(IAuthenticationContext authenticationContext, IDatabaseContext databaseContext, ILogger< LoginAuthority > logger, IApiHeadersProvider apiHeadersProvider, ISystemIdentityFactory systemIdentityFactory, IOAuthProviders oAuthProviders, ITokenFactory tokenFactory, ICryptographySuite cryptographySuite, IIdentityCache identityCache, ISessionInvalidationTracker sessionInvalidationTracker)
Initializes a new instance of the LoginAuthority class.
readonly ISystemIdentityFactory systemIdentityFactory
The ISystemIdentityFactory for the LoginAuthority.
static async ValueTask< User?> SelectUserInfoFromQuery(IQueryable< User > query, CancellationToken cancellationToken)
Select the details needed to generate a TokenResponse from a given query .
static AuthorityResponse< TResult > GenerateHeadersExceptionResponse< TResult >(HeadersException headersException)
Generate an AuthorityResponse<TResult> for a given headersException .
async ValueTask< AuthorityResponse< OAuthGatewayLoginResult > > AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
Attempt to login to an OAuth service with the current OAuth credentials.A ValueTask<TResult> resultin...
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the LoginAuthority.
readonly ISessionInvalidationTracker sessionInvalidationTracker
The ISessionInvalidationTracker for the LoginAuthority.
readonly ITokenFactory tokenFactory
The ITokenFactory for the LoginAuthority.
Backend abstract implementation of IDatabaseContext.
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext.A Task representing the running operation.
DbSet< User > Users
The Users in the DatabaseContext.
required string AccessCode
The user's access token for the requested OAuth service.
ITransformer<TInput, TOutput> for GraphQL.Types.Users.
const string TgsSystemUserName
Username used when creating jobs automatically.
Definition User.cs:21
static string CanonicalizeName(string name)
Change a UserName.Name into a CanonicalName.
IAuthority for authenticating with the server.
Represents a host-side model that may be transformed into a TApiModel .
For creating and accessing authentication contexts.
Contains various cryptographic functions.
bool CheckUserPassword(User user, string password)
Checks a given password matches a given user 's User.PasswordHash. This may result in User....
ValueTask CacheSystemIdentity(User user, ISystemIdentity systemIdentity, DateTimeOffset expiry)
Keep a user 's systemIdentity alive until an expiry time.
void UserModifiedInvalidateSessions(User user)
Invalidate all sessions for a given user .
Task< ISystemIdentity?> CreateSystemIdentity(User user, CancellationToken cancellationToken)
Create a ISystemIdentity for a given user .
Represents a user on the current global::System.Runtime.InteropServices.OSPlatform.
string Uid
A unique identifier for the user.
TokenValidationParameters ValidationParameters
The TokenValidationParameters for the ITokenFactory.
string CreateToken(Models.User user, bool oAuth)
Create a TokenResponse for a given user .
IOAuthValidator? GetValidator(OAuthProvider oAuthProvider, bool forLogin)
Gets the IOAuthValidator for a given oAuthProvider .
ValueTask<(string? UserID, string AccessCode)?> ValidateResponseCode(string code, bool requireUserID, CancellationToken cancellationToken)
Validate a given OAuth response code .
ApiHeaders? ApiHeaders
The created Api.ApiHeaders, if any.
HeadersException? HeadersException
The Api.HeadersException thrown when attempting to parse the ApiHeaders if any.
ErrorCode
Types of Response.ErrorMessageResponses that the API may return.
Definition ErrorCode.cs:12
OAuthProvider
List of OAuth providers supported by TGS.
HeaderErrorTypes
Types of individual ApiHeaders errors.
HttpFailureResponse
Indicates the type of HTTP status code an failing AuthorityResponse should generate.
@ Id
Lookup the Api.Models.EntityId.Id of the Models.PermissionSet.