tgstation-server 6.16.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
AuthenticationContextFactory.cs
Go to the documentation of this file.
1using System;
4using System.Linq;
8
16
24
26{
29 {
33 public const string OpenIDConnectAuthenticationSchemePrefix = $"{OpenIdConnectDefaults.AuthenticationScheme}.";
34
39
44
49
54
59
64
69
74
79
86 static DateTimeOffset ParseTime(ClaimsPrincipal principal, string key)
87 {
88 var claim = principal.FindFirst(key);
89 if (claim == default)
90 throw new InvalidOperationException($"Missing '{key}' claim!");
91
92 try
93 {
94 return new DateTimeOffset(
95 EpochTime.DateTime(
96 Int64.Parse(claim.Value, CultureInfo.InvariantCulture)));
97 }
98 catch (Exception ex)
99 {
100 throw new InvalidOperationException($"Failed to parse '{key}'!", ex);
101 }
102 }
103
133
136
138 #pragma warning disable CA1506 // TODO: Decomplexify
139 public async Task ValidateTgsToken(Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
140 #pragma warning restore CA1506
141 {
143
144 if (tokenValidatedContext.SecurityToken is not JsonWebToken jwt)
145 throw new ArgumentException($"Expected {nameof(tokenValidatedContext)} to contain a {nameof(JsonWebToken)}!", nameof(tokenValidatedContext));
146
147 if (Interlocked.Exchange(ref initialized, 1) != 0)
148 throw new InvalidOperationException("Authentication context has already been loaded");
149
151
153 if (userIdClaim == default)
154 throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!");
155
156 long userId;
157 try
158 {
159 userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
160 }
161 catch (Exception e)
162 {
163 throw new InvalidOperationException("Failed to parse user ID!", e);
164 }
165
168
170 .Users
171 .AsQueryable()
172 .Where(x => x.Id == userId)
173 .Include(x => x.CreatedBy)
174 .Include(x => x.PermissionSet)
175 .Include(x => x.Group)
176 .ThenInclude(x => x!.PermissionSet)
177 .Include(x => x.OAuthConnections)
178 .FirstOrDefaultAsync(cancellationToken);
179 if (user == default)
180 {
181 tokenValidatedContext.Fail($"Unable to find user with ID {userId}!");
182 return;
183 }
184
186 if (user.SystemIdentifier != null)
188 else
189 {
190 if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate >= notBefore)
191 {
192 tokenValidatedContext.Fail($"Rejecting token for user {userId} created before last modification: {user.LastPasswordUpdate.Value}");
193 return;
194 }
195
196 systemIdentity = null;
197 }
198
199 var userPermissionSet = user.PermissionSet ?? user.Group!.PermissionSet;
200 try
201 {
204 if (instanceId.HasValue)
205 {
207 .AsQueryable()
208 .Where(x => x.PermissionSetId == userPermissionSet!.Id && x.InstanceId == instanceId && x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier)
209 .Include(x => x.Instance)
210 .FirstOrDefaultAsync(cancellationToken);
211
212 if (instancePermissionSet == null)
213 logger.LogDebug("User {userId} does not have permissions on instance {instanceId}!", userId, instanceId.Value);
214 }
215
217 user,
218 expires,
219 jwt.EncodedSignature, // signature is enough to uniquely identify the session as it is composite of all the inputs
222 }
223 catch
224 {
225 systemIdentity?.Dispose();
226 throw;
227 }
228 }
229
231#pragma warning disable CA1506 // TODO: Decomplexify
233#pragma warning restore CA1506
234 {
236
238 if (principal == null)
239 throw new InvalidOperationException("Expected a valid principal here!");
240
242 if (userIdClaim == default)
243 throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!");
244
245 var userId = userIdClaim.Value;
246 var scheme = tokenValidatedContext.Scheme.Name;
250 .AsQueryable()
251 .Where(oidcConnection => oidcConnection.ExternalUserId == userId && oidcConnection.SchemeKey == deprefixedScheme)
252 .Include(oidcConnection => oidcConnection.User)
253 .ThenInclude(user => user!.Group)
254 .ThenInclude(group => group!.PermissionSet)
255 .FirstOrDefaultAsync(cancellationToken);
256
257 User user;
259 {
260 if (connection == default)
261 {
262 tokenValidatedContext.Fail($"Unable to find user with OidcConnection for {deprefixedScheme}/{userId}!");
263 return;
264 }
265
266 user = connection.User!;
267 }
268 else
269 {
271 long? groupId;
272 if (groupClaim == default)
273 groupId = null;
274 else if (Int64.TryParse(groupClaim.Value, out long groupIdParsed))
276 else
277 {
278 tokenValidatedContext.Fail($"User has non-numeric '{groupIdClaimName}' claim!");
279 return;
280 }
281
284 .Groups
285 .AsQueryable()
286 .Where(group => group.Id == groupId.Value)
287 .Include(group => group.PermissionSet)
288 .FirstOrDefaultAsync(cancellationToken)
289 : null;
290
291 var missingClaimError = $"User missing '{groupIdClaimName}' claim!";
292 if (connection == default)
293 {
294 var username = principal.Identity?.Name;
295 if (username == null)
296 {
297 tokenValidatedContext.Fail("Failed to retrieve user's name from retrieved claims!");
298 return;
299 }
300
301 if (username.Contains(':', StringComparison.Ordinal))
302 {
303 tokenValidatedContext.Fail("Cannot create users with the ':' in their name!");
304 return;
305 }
306
307 if (group == null)
308 {
310 groupId.HasValue
311 ? $"'{groupIdClaimName}' does not point to a valid group!"
313 return;
314 }
315
316 logger.LogInformation("Registering new user '{name}' via OIDC scheme '{scheme}'", username, schemeKey);
317
319 .Users
320 .GetTgsUser(
321 dbUser => new User
322 {
323 Id = dbUser.Id!.Value,
324 },
326
327 user = new User
328 {
329 CreatedAt = DateTimeOffset.UtcNow,
330 CanonicalName = User.CanonicalizeName(username),
331 Name = username,
332 CreatedById = tgsUser.Id,
333 Enabled = true,
334 GroupId = group.Id,
335 OidcConnections = new List<OidcConnection>
336 {
337 new()
338 {
339 SchemeKey = schemeKey,
340 ExternalUserId = userId,
341 },
342 },
343 PasswordHash = "_", // This can't be hashed
344 };
345
347 }
348 else
349 {
350 user = connection.User!;
351
352 // group update
353 if (group == null)
354 {
355 logger.LogDebug("User {id} attempted to login via OIDC scheme '{scheme}' but had no group ID claim ('{groupClaimName}') and will be disabled", user.Id, schemeKey, groupIdClaimName);
357 {
360 };
361 user.GroupId = null;
362 user.Enabled = false;
363
365 return;
366 }
367
368 logger.LogDebug("User {id} mapped to group {groupId} via OIDC login on scheme '{scheme}'", user.Id, groupId, schemeKey);
370 if (user.PermissionSet != null)
372
373 user.Enabled = true;
374 }
375
377 }
378
380
382 user,
383 expires,
384 Guid.NewGuid().ToString(),
385 null,
386 null);
387 }
388 }
389}
Represents the header that must be present for every server request.
Definition ApiHeaders.cs:25
long? InstanceId
The instance EntityId.Id being accessed.
Definition ApiHeaders.cs:84
virtual ? long Id
The ID of the entity.
Definition EntityId.cs:14
string? Identifier
The server's identifier.
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...
Configuration for the server swarm system.
Represents a group of Users.
Definition UserGroup.cs:16
static string CanonicalizeName(string name)
Change a UserName.Name into a CanonicalName.
PermissionSet? PermissionSet
The PermissionSet the User has, if any.
Definition User.cs:51
AuthenticationContextFactory(IDatabaseContext databaseContext, IIdentityCache identityCache, IApiHeadersProvider apiHeadersProvider, IOptions< SwarmConfiguration > swarmConfigurationOptions, IOptions< SecurityConfiguration > securityConfigurationOptions, ILogger< AuthenticationContextFactory > logger)
Initializes a new instance of the AuthenticationContextFactory class.
readonly SecurityConfiguration securityConfiguration
The SecurityConfiguration for the AuthenticationContextFactory.
IAuthenticationContext CurrentAuthenticationContext
The IAuthenticationContext the AuthenticationContextFactory created.
readonly IDatabaseContext databaseContext
The IDatabaseContext for the AuthenticationContextFactory.
readonly ILogger< AuthenticationContextFactory > logger
The ILogger for the AuthenticationContextFactory.
static DateTimeOffset ParseTime(ClaimsPrincipal principal, string key)
Parse a DateTimeOffset out of a Claim in a given principal .
int initialized
1 if currentAuthenticationContext was initialized, 0 otherwise.
readonly SwarmConfiguration swarmConfiguration
The SwarmConfiguration for the AuthenticationContextFactory.
readonly IIdentityCache identityCache
The IIdentityCache for the AuthenticationContextFactory.
readonly? ApiHeaders apiHeaders
The ApiHeaders for the AuthenticationContextFactory.
const string OpenIDConnectAuthenticationSchemePrefix
Internal scheme prefix for OIDC schemes.
async Task ValidateOidcToken(RemoteAuthenticationContext< OpenIdConnectOptions > tokenValidatedContext, string schemeKey, string groupIdClaimName, CancellationToken cancellationToken)
Handles OIDC tokenValidatedContext s.A Task representing the running operation.
readonly AuthenticationContext currentAuthenticationContext
Backing field for CurrentAuthenticationContext.
async Task ValidateTgsToken(Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
Handles TGS tokenValidatedContext s.A Task representing the running operation.
void Initialize(User user, DateTimeOffset sessionExpiry, string sessionId, InstancePermissionSet? instanceUser, ISystemIdentity? systemIdentity)
Initializes the AuthenticationContext.
void Add(TModel model)
Add a given model to the the working set.
void Remove(TModel model)
Remove a given model from the the working set.
IDatabaseCollection< PermissionSet > PermissionSets
The DbSet<TEntity> for PermissionSets.
IDatabaseCollection< OidcConnection > OidcConnections
The DbSet<TEntity> for OidcConnections.
Task Save(CancellationToken cancellationToken)
Saves changes made to the IDatabaseContext.
IDatabaseCollection< UserGroup > Groups
The DbSet<TEntity> for UserGroups.
IDatabaseCollection< User > Users
The Users in the IDatabaseContext.
For creating and accessing authentication contexts.
ISystemIdentity LoadCachedIdentity(User user)
Attempt to load a cached ISystemIdentity.
Represents a user on the current global::System.Runtime.InteropServices.OSPlatform.
Handles validating authentication tokens.
ApiHeaders? ApiHeaders
The created Api.ApiHeaders, if any.
@ List
User may list files if the Models.Instance allows it.
InstanceManagerRights
Rights for managing Models.Instances.
AdministrationRights
Administration rights for the server.
@ Enabled
The OAuth Gateway is enabled.