tgstation-server 6.19.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.AspNetCore.Authorization;
7using Microsoft.EntityFrameworkCore;
8using Microsoft.Extensions.Logging;
9using Microsoft.Extensions.Options;
10
23
25{
28 {
33
38
43
48
53
58
63
68
76 => new(
77 new ErrorMessageResponse(ErrorCode.BadHeaders)
78 {
79 AdditionalData = headersException.Message,
80 },
81 headersException.ParseErrors.HasFlag(HeaderErrorTypes.Accept)
82 ? HttpFailureResponse.NotAcceptable
83 : HttpFailureResponse.BadRequest);
84
91 static async ValueTask<User?> SelectUserInfoFromQuery(IQueryable<User> query, CancellationToken cancellationToken)
92 {
93 var users = await query
94 .ToListAsync(cancellationToken);
95
96 // Pick the DB user first
97 var user = users
98 .OrderByDescending(dbUser => dbUser.SystemIdentifier == null)
99 .FirstOrDefault();
100
101 return user;
102 }
103
118 IDatabaseContext databaseContext,
119 ILogger<LoginAuthority> logger,
127 IOptions<SecurityConfiguration> securityConfigurationOptions)
128 : base(
129 databaseContext,
130 logger)
131 {
132 this.apiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider));
133 this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
134 this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders));
135 this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
136 this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
137 this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
138 this.sessionInvalidationTracker = sessionInvalidationTracker ?? throw new ArgumentNullException(nameof(sessionInvalidationTracker));
139 securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
140 }
141
143 public RequirementsGated<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken)
144 => new(
145 () => null,
146 () => AttemptLoginImpl(cancellationToken),
147 true);
148
151 => new(
152 () => (IAuthorizationRequirement?)null,
153 async () =>
154 {
155 var headers = apiHeadersProvider.ApiHeaders;
156 if (headers == null)
157 return GenerateHeadersExceptionResponse<OAuthGatewayLoginResult>(apiHeadersProvider.HeadersException!);
158
159 var oAuthProvider = headers.OAuthProvider;
160 if (!oAuthProvider.HasValue)
161 return BadRequest<OAuthGatewayLoginResult>(ErrorCode.BadHeaders);
162
163 var (errorResponse, oAuthResult) = await TryOAuthenticate<OAuthGatewayLoginResult>(headers, oAuthProvider.Value, false, cancellationToken);
164 if (errorResponse != null)
165 return errorResponse;
166
167 Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value);
168
169 return new(
171 {
172 AccessCode = oAuthResult!.Value.AccessCode,
173 });
174 });
175
181 private async ValueTask<AuthorityResponse<LoginResult>> AttemptLoginImpl(CancellationToken cancellationToken)
182 {
183 // password and oauth logins disabled
185 return Unauthorized<LoginResult>();
186
187 var headers = apiHeadersProvider.ApiHeaders;
188 if (headers == null)
189 return GenerateHeadersExceptionResponse<LoginResult>(apiHeadersProvider.HeadersException!);
190
191 if (headers.IsTokenAuthentication)
192 return BadRequest<LoginResult>(ErrorCode.TokenWithToken);
193
194 var oAuthLogin = headers.OAuthProvider.HasValue;
195
196 ISystemIdentity? systemIdentity = null;
197 if (!oAuthLogin)
198 try
199 {
200 // trust the system over the database because a user's name can change while still having the same SID
201 systemIdentity = await systemIdentityFactory.CreateSystemIdentity(headers.Username!, headers.Password!, cancellationToken);
202 }
203 catch (NotImplementedException)
204 {
205 // Intentionally suppressed
206 }
207
208 using (systemIdentity)
209 {
210 // Get the user from the database
211 IQueryable<User> query = DatabaseContext.Users.AsQueryable();
212 if (oAuthLogin)
213 {
214 var oAuthProvider = headers.OAuthProvider!.Value;
215 var (errorResponse, oauthResult) = await TryOAuthenticate<LoginResult>(headers, oAuthProvider, true, cancellationToken);
216 if (errorResponse != null)
217 return errorResponse;
218
219 query = query.Where(
220 x => x.OAuthConnections!.Any(
221 y => y.Provider == oAuthProvider
222 && y.ExternalUserId == oauthResult!.Value.UserID));
223 }
224 else
225 {
226 var canonicalUserName = User.CanonicalizeName(headers.Username!);
227 if (canonicalUserName == User.CanonicalizeName(User.TgsSystemUserName))
228 return Unauthorized<LoginResult>();
229
230 if (systemIdentity == null)
231 query = query.Where(x => x.CanonicalName == canonicalUserName);
232 else
233 query = query.Where(x => x.CanonicalName == canonicalUserName || x.SystemIdentifier == systemIdentity.Uid);
234 }
235
236 var user = await SelectUserInfoFromQuery(query, cancellationToken);
237
238 // No user? You're not allowed
239 if (user == null)
240 return Unauthorized<LoginResult>();
241
242 // A system user may have had their name AND password changed to one in our DB...
243 // Or a DB user was created that had the same user/pass as a system user
244 // Dumb admins...
245 // FALLBACK TO THE DB USER HERE, DO NOT REVEAL A SYSTEM LOGIN!!!
246 // This of course, allows system users to discover TGS users in this (HIGHLY IMPROBABLE) case but that is not our fault
247 var originalHash = user.PasswordHash;
248 var isLikelyDbUser = originalHash != null;
249 var usingSystemIdentity = systemIdentity != null && !isLikelyDbUser;
250 if (!oAuthLogin)
251 if (!usingSystemIdentity)
252 {
253 // DB User password check and update
254 if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, headers.Password!))
255 return Unauthorized<LoginResult>();
256 if (user.PasswordHash != originalHash)
257 {
258 Logger.LogDebug("User ID {userId}'s password hash needs a refresh, updating database.", user.Id);
259 var updatedUser = new User
260 {
261 Id = user.Id,
262 };
263 DatabaseContext.Users.Attach(updatedUser);
264 updatedUser.PasswordHash = user.PasswordHash;
265 await DatabaseContext.Save(cancellationToken);
266 }
267 }
268 else
269 {
270 var usernameMismatch = systemIdentity!.Username != user.Name;
271 if (isLikelyDbUser || usernameMismatch)
272 {
273 DatabaseContext.Users.Attach(user);
274 if (usernameMismatch)
275 {
276 // System identity username change update
277 Logger.LogDebug("User ID {userId}'s system identity needs a refresh, updating database.", user.Id);
278 user.Name = systemIdentity.Username;
279 user.CanonicalName = User.CanonicalizeName(user.Name);
280 }
281
282 if (isLikelyDbUser)
283 {
284 // cleanup from https://github.com/tgstation/tgstation-server/issues/1528
285 Logger.LogDebug("System user ID {userId}'s PasswordHash is polluted, updating database.", user.Id);
286 user.PasswordHash = null;
288 }
289
290 await DatabaseContext.Save(cancellationToken);
291 }
292 }
293
294 // Now that the bookeeping is done, tell them to fuck off if necessary
295 if (!user.Enabled!.Value)
296 {
297 Logger.LogTrace("Not logging in disabled user {userId}.", user.Id);
298 return Forbid<LoginResult>();
299 }
300
301 var token = tokenFactory.CreateToken(user, oAuthLogin);
302 var payload = new LoginResult
303 {
304 Bearer = token,
305 User = ((IApiTransformable<User, GraphQL.Types.User, UserGraphQLTransformer>)user).ToApi(),
306 };
307
308 if (usingSystemIdentity)
309 await CacheSystemIdentity(systemIdentity!, user, payload);
310
311 Logger.LogDebug("Successfully logged in user {userId}!", user.Id);
312
313 return new AuthorityResponse<LoginResult>(payload);
314 }
315 }
316
324 private async ValueTask CacheSystemIdentity(ISystemIdentity systemIdentity, User user, LoginResult loginPayload)
325 {
326 // expire the identity slightly after the auth token in case of lag
327 var identExpiry = loginPayload.ToApi().ParseJwt().ValidTo;
328 identExpiry += tokenFactory.ValidationParameters.ClockSkew;
329 identExpiry += TimeSpan.FromSeconds(15);
330 await identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry);
331 }
332
342 async ValueTask<(AuthorityResponse<TResult>? ErrorResponse, (string? UserID, string AccessCode)? OAuthResult)> TryOAuthenticate<TResult>(ApiHeaders headers, OAuthProvider oAuthProvider, bool forLogin, CancellationToken cancellationToken)
343 {
344 (string? UserID, string AccessCode)? oauthResult;
345 try
346 {
347 // minor special case here until its removal
348#pragma warning disable CS0618 // Type or member is obsolete
349 if (oAuthProvider == OAuthProvider.TGForums)
350#pragma warning restore CS0618 // Type or member is obsolete
351 return (Unauthorized<TResult>(), null);
352
353 var validator = oAuthProviders
354 .GetValidator(oAuthProvider, forLogin);
355
356 if (validator == null)
357 return (BadRequest<TResult>(ErrorCode.OAuthProviderDisabled), null);
358 oauthResult = await validator
359 .ValidateResponseCode(headers.OAuthCode!, forLogin, cancellationToken);
360
361 Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, oauthResult);
362 }
363 catch (Octokit.RateLimitExceededException ex)
364 {
365 return (RateLimit<TResult>(ex), null);
366 }
367
368 if (!oauthResult.HasValue)
369 return (Unauthorized<TResult>(), null);
370
371 return (null, OAuthResult: oauthResult);
372 }
373 }
374}
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:14
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>.
Evaluates a set of IAuthorizationRequirements to be checked before executing a response.
async ValueTask< AuthorityResponse< LoginResult > > AttemptLoginImpl(CancellationToken cancellationToken)
Login process.
LoginAuthority(IDatabaseContext databaseContext, ILogger< LoginAuthority > logger, IApiHeadersProvider apiHeadersProvider, ISystemIdentityFactory systemIdentityFactory, IOAuthProviders oAuthProviders, ITokenFactory tokenFactory, ICryptographySuite cryptographySuite, IIdentityCache identityCache, ISessionInvalidationTracker sessionInvalidationTracker, IOptions< SecurityConfiguration > securityConfigurationOptions)
Initializes a new instance of the LoginAuthority class.
readonly IApiHeadersProvider apiHeadersProvider
The IApiHeadersProvider for the LoginAuthority.
RequirementsGated< 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.
RequirementsGated< AuthorityResponse< OAuthGatewayLoginResult > > AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
Attempt to login to an OAuth service with the current OAuth credentials.A ValueTask<TResult> resultin...
async ValueTask<(AuthorityResponse< TResult >? ErrorResponse,(string? UserID, string AccessCode)? OAuthResult)> TryOAuthenticate< TResult >(ApiHeaders headers, OAuthProvider oAuthProvider, bool forLogin, CancellationToken cancellationToken)
Attempt OAuth authentication.
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 .
readonly SecurityConfiguration securityConfiguration
The SecurityConfiguration for the LoginAuthority.
static AuthorityResponse< TResult > GenerateHeadersExceptionResponse< TResult >(HeadersException headersException)
Generate an AuthorityResponse<TResult> for a given headersException .
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the LoginAuthority.
readonly ISessionInvalidationTracker sessionInvalidationTracker
The ISessionInvalidationTracker for the LoginAuthority.
readonly ITokenFactory tokenFactory
The ITokenFactory for the LoginAuthority.
Configuration options pertaining to user security.
bool OidcStrictMode
If OIDC strict mode should be enabled. This mode enforces the existence of at least one OpenIDConnect...
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 .
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.
string CreateToken(Models.User user, bool serviceLogin)
Create a TokenResponse for a given user .
TokenValidationParameters ValidationParameters
The TokenValidationParameters for the ITokenFactory.
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 OAuth2.0 providers supported by TGS that do not support OIDC.
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.