tgstation-server 6.16.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;
8using Microsoft.Extensions.Options;
9
22
24{
27 {
32
37
42
47
52
57
62
67
75 => new(
76 new ErrorMessageResponse(ErrorCode.BadHeaders)
77 {
78 AdditionalData = headersException.Message,
79 },
80 headersException.ParseErrors.HasFlag(HeaderErrorTypes.Accept)
81 ? HttpFailureResponse.NotAcceptable
82 : HttpFailureResponse.BadRequest);
83
90 static async ValueTask<User?> SelectUserInfoFromQuery(IQueryable<User> query, CancellationToken cancellationToken)
91 {
92 var users = await query
93 .ToListAsync(cancellationToken);
94
95 // Pick the DB user first
96 var user = users
97 .OrderByDescending(dbUser => dbUser.SystemIdentifier == null)
98 .FirstOrDefault();
99
100 return user;
101 }
102
118 IAuthenticationContext authenticationContext,
119 IDatabaseContext databaseContext,
120 ILogger<LoginAuthority> logger,
128 IOptions<SecurityConfiguration> securityConfigurationOptions)
129 : base(
130 authenticationContext,
131 databaseContext,
132 logger)
133 {
134 this.apiHeadersProvider = apiHeadersProvider ?? throw new ArgumentNullException(nameof(apiHeadersProvider));
135 this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
136 this.oAuthProviders = oAuthProviders ?? throw new ArgumentNullException(nameof(oAuthProviders));
137 this.tokenFactory = tokenFactory ?? throw new ArgumentNullException(nameof(tokenFactory));
138 this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
139 this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
140 this.sessionInvalidationTracker = sessionInvalidationTracker ?? throw new ArgumentNullException(nameof(sessionInvalidationTracker));
141 securityConfiguration = securityConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(securityConfigurationOptions));
142 }
143
145 public async ValueTask<AuthorityResponse<LoginResult>> AttemptLogin(CancellationToken cancellationToken)
146 {
147 // password and oauth logins disabled
149 return Unauthorized<LoginResult>();
150
151 var headers = apiHeadersProvider.ApiHeaders;
152 if (headers == null)
153 return GenerateHeadersExceptionResponse<LoginResult>(apiHeadersProvider.HeadersException!);
154
155 if (headers.IsTokenAuthentication)
156 return BadRequest<LoginResult>(ErrorCode.TokenWithToken);
157
158 var oAuthLogin = headers.OAuthProvider.HasValue;
159
160 ISystemIdentity? systemIdentity = null;
161 if (!oAuthLogin)
162 try
163 {
164 // trust the system over the database because a user's name can change while still having the same SID
165 systemIdentity = await systemIdentityFactory.CreateSystemIdentity(headers.Username!, headers.Password!, cancellationToken);
166 }
167 catch (NotImplementedException)
168 {
169 // Intentionally suppressed
170 }
171
172 using (systemIdentity)
173 {
174 // Get the user from the database
175 IQueryable<User> query = DatabaseContext.Users.AsQueryable();
176 if (oAuthLogin)
177 {
178 var oAuthProvider = headers.OAuthProvider!.Value;
179 var (errorResponse, oauthResult) = await TryOAuthenticate<LoginResult>(headers, oAuthProvider, true, cancellationToken);
180 if (errorResponse != null)
181 return errorResponse;
182
183 query = query.Where(
184 x => x.OAuthConnections!.Any(
185 y => y.Provider == oAuthProvider
186 && y.ExternalUserId == oauthResult!.Value.UserID));
187 }
188 else
189 {
190 var canonicalUserName = User.CanonicalizeName(headers.Username!);
191 if (canonicalUserName == User.CanonicalizeName(User.TgsSystemUserName))
192 return Unauthorized<LoginResult>();
193
194 if (systemIdentity == null)
195 query = query.Where(x => x.CanonicalName == canonicalUserName);
196 else
197 query = query.Where(x => x.CanonicalName == canonicalUserName || x.SystemIdentifier == systemIdentity.Uid);
198 }
199
200 var user = await SelectUserInfoFromQuery(query, cancellationToken);
201
202 // No user? You're not allowed
203 if (user == null)
204 return Unauthorized<LoginResult>();
205
206 // A system user may have had their name AND password changed to one in our DB...
207 // Or a DB user was created that had the same user/pass as a system user
208 // Dumb admins...
209 // FALLBACK TO THE DB USER HERE, DO NOT REVEAL A SYSTEM LOGIN!!!
210 // This of course, allows system users to discover TGS users in this (HIGHLY IMPROBABLE) case but that is not our fault
211 var originalHash = user.PasswordHash;
212 var isLikelyDbUser = originalHash != null;
213 var usingSystemIdentity = systemIdentity != null && !isLikelyDbUser;
214 if (!oAuthLogin)
215 if (!usingSystemIdentity)
216 {
217 // DB User password check and update
218 if (!isLikelyDbUser || !cryptographySuite.CheckUserPassword(user, headers.Password!))
219 return Unauthorized<LoginResult>();
220 if (user.PasswordHash != originalHash)
221 {
222 Logger.LogDebug("User ID {userId}'s password hash needs a refresh, updating database.", user.Id);
223 var updatedUser = new User
224 {
225 Id = user.Id,
226 };
227 DatabaseContext.Users.Attach(updatedUser);
228 updatedUser.PasswordHash = user.PasswordHash;
229 await DatabaseContext.Save(cancellationToken);
230 }
231 }
232 else
233 {
234 var usernameMismatch = systemIdentity!.Username != user.Name;
235 if (isLikelyDbUser || usernameMismatch)
236 {
237 DatabaseContext.Users.Attach(user);
238 if (usernameMismatch)
239 {
240 // System identity username change update
241 Logger.LogDebug("User ID {userId}'s system identity needs a refresh, updating database.", user.Id);
242 user.Name = systemIdentity.Username;
243 user.CanonicalName = User.CanonicalizeName(user.Name);
244 }
245
246 if (isLikelyDbUser)
247 {
248 // cleanup from https://github.com/tgstation/tgstation-server/issues/1528
249 Logger.LogDebug("System user ID {userId}'s PasswordHash is polluted, updating database.", user.Id);
250 user.PasswordHash = null;
252 }
253
254 await DatabaseContext.Save(cancellationToken);
255 }
256 }
257
258 // Now that the bookeeping is done, tell them to fuck off if necessary
259 if (!user.Enabled!.Value)
260 {
261 Logger.LogTrace("Not logging in disabled user {userId}.", user.Id);
262 return Forbid<LoginResult>();
263 }
264
265 var token = tokenFactory.CreateToken(user, oAuthLogin);
266 var payload = new LoginResult
267 {
268 Bearer = token,
269 User = ((IApiTransformable<User, GraphQL.Types.User, UserGraphQLTransformer>)user).ToApi(),
270 };
271
272 if (usingSystemIdentity)
273 await CacheSystemIdentity(systemIdentity!, user, payload);
274
275 Logger.LogDebug("Successfully logged in user {userId}!", user.Id);
276
277 return new AuthorityResponse<LoginResult>(payload);
278 }
279 }
280
282 public async ValueTask<AuthorityResponse<OAuthGatewayLoginResult>> AttemptOAuthGatewayLogin(CancellationToken cancellationToken)
283 {
284 var headers = apiHeadersProvider.ApiHeaders;
285 if (headers == null)
286 return GenerateHeadersExceptionResponse<OAuthGatewayLoginResult>(apiHeadersProvider.HeadersException!);
287
288 var oAuthProvider = headers.OAuthProvider;
289 if (!oAuthProvider.HasValue)
290 return BadRequest<OAuthGatewayLoginResult>(ErrorCode.BadHeaders);
291
292 var (errorResponse, oAuthResult) = await TryOAuthenticate<OAuthGatewayLoginResult>(headers, oAuthProvider.Value, false, cancellationToken);
293 if (errorResponse != null)
294 return errorResponse;
295
296 Logger.LogDebug("Generated {provider} OAuth AccessCode", oAuthProvider.Value);
297
300 {
301 AccessCode = oAuthResult!.Value.AccessCode,
302 });
303 }
304
312 private async ValueTask CacheSystemIdentity(ISystemIdentity systemIdentity, User user, LoginResult loginPayload)
313 {
314 // expire the identity slightly after the auth token in case of lag
315 var identExpiry = loginPayload.ToApi().ParseJwt().ValidTo;
316 identExpiry += tokenFactory.ValidationParameters.ClockSkew;
317 identExpiry += TimeSpan.FromSeconds(15);
318 await identityCache.CacheSystemIdentity(user, systemIdentity!, identExpiry);
319 }
320
330 async ValueTask<(AuthorityResponse<TResult>? ErrorResponse, (string? UserID, string AccessCode)? OAuthResult)> TryOAuthenticate<TResult>(ApiHeaders headers, OAuthProvider oAuthProvider, bool forLogin, CancellationToken cancellationToken)
331 {
332 (string? UserID, string AccessCode)? oauthResult;
333 try
334 {
335 // minor special case here until its removal
336#pragma warning disable CS0618 // Type or member is obsolete
337 if (oAuthProvider == OAuthProvider.TGForums)
338#pragma warning restore CS0618 // Type or member is obsolete
339 return (Unauthorized<TResult>(), null);
340
341 var validator = oAuthProviders
342 .GetValidator(oAuthProvider, forLogin);
343
344 if (validator == null)
345 return (BadRequest<TResult>(ErrorCode.OAuthProviderDisabled), null);
346 oauthResult = await validator
347 .ValidateResponseCode(headers.OAuthCode!, forLogin, cancellationToken);
348
349 Logger.LogTrace("External {oAuthProvider} UID: {externalUserId}", oAuthProvider, oauthResult);
350 }
351 catch (Octokit.RateLimitExceededException ex)
352 {
353 return (RateLimit<TResult>(ex), null);
354 }
355
356 if (!oauthResult.HasValue)
357 return (Unauthorized<TResult>(), null);
358
359 return (null, OAuthResult: oauthResult);
360 }
361 }
362}
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>.
readonly IApiHeadersProvider apiHeadersProvider
The IApiHeadersProvider for the LoginAuthority.
LoginAuthority(IAuthenticationContext authenticationContext, 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.
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.
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 .
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.
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 .
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.
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.