tgstation-server 6.19.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
AuthenticationContextFactory.cs
Go to the documentation of this file.
1using System;
3using System.Linq;
7
14
22
24{
27 {
31 public const string OpenIDConnectAuthenticationSchemePrefix = $"{OpenIdConnectDefaults.AuthenticationScheme}.";
32
37
42
47
52
57
62
67
72
77
107
110
112 #pragma warning disable CA1506 // TODO: Decomplexify
113 public async Task ValidateTgsToken(Microsoft.AspNetCore.Authentication.JwtBearer.TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
114 #pragma warning restore CA1506
115 {
117
118 if (tokenValidatedContext.SecurityToken is not JsonWebToken jwt)
119 throw new ArgumentException($"Expected {nameof(tokenValidatedContext)} to contain a {nameof(JsonWebToken)}!", nameof(tokenValidatedContext));
120
121 if (Interlocked.Exchange(ref initialized, 1) != 0)
122 throw new InvalidOperationException("Authentication context has already been loaded");
123
125 var userId = principal.GetTgsUserId();
126
129
131 .Users
132 .AsQueryable()
133 .Where(x => x.Id == userId)
134 .Include(x => x.CreatedBy)
135 .Include(x => x.PermissionSet)
136 .Include(x => x.Group)
137 .ThenInclude(x => x!.PermissionSet)
138 .Include(x => x.OAuthConnections)
139 .FirstOrDefaultAsync(cancellationToken);
140 if (user == default)
141 {
142 tokenValidatedContext.Fail($"Unable to find user with ID {userId}!");
143 return;
144 }
145
147 if (user.SystemIdentifier != null)
149 else
150 {
151 if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate >= notBefore)
152 {
153 tokenValidatedContext.Fail($"Rejecting token for user {userId} created before last modification: {user.LastPasswordUpdate.Value}");
154 return;
155 }
156
157 systemIdentity = null;
158 }
159
160 var userPermissionSet = user.PermissionSet ?? user.Group!.PermissionSet;
161 try
162 {
165 if (instanceId.HasValue)
166 {
168 .AsQueryable()
169 .Where(x => x.PermissionSetId == userPermissionSet!.Id && x.InstanceId == instanceId && x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier)
170 .Include(x => x.Instance)
171 .FirstOrDefaultAsync(cancellationToken);
172
173 if (instancePermissionSet == null)
174 logger.LogDebug("User {userId} does not have permissions on instance {instanceId}!", userId, instanceId.Value);
175 }
176
178 user,
179 expires,
180 jwt.EncodedSignature, // signature is enough to uniquely identify the session as it is composite of all the inputs
183 }
184 catch
185 {
186 systemIdentity?.Dispose();
187 throw;
188 }
189 }
190
192#pragma warning disable CA1506 // TODO: Decomplexify
194#pragma warning restore CA1506
195 {
197
199 if (principal == null)
200 throw new InvalidOperationException("Expected a valid principal here!");
201
203 if (userIdClaim == default)
204 throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!");
205
206 var userId = userIdClaim.Value;
207 var scheme = tokenValidatedContext.Scheme.Name;
211 .AsQueryable()
212 .Where(oidcConnection => oidcConnection.ExternalUserId == userId && oidcConnection.SchemeKey == deprefixedScheme)
213 .Include(oidcConnection => oidcConnection.User)
214 .ThenInclude(user => user!.Group)
215 .ThenInclude(group => group!.PermissionSet)
216 .FirstOrDefaultAsync(cancellationToken);
217
218 User user;
220 {
221 if (connection == default)
222 {
223 tokenValidatedContext.Fail($"Unable to find user with OidcConnection for {deprefixedScheme}/{userId}!");
224 return;
225 }
226
227 user = connection.User!;
228 }
229 else
230 {
232 long? groupId;
233 if (groupClaim == default)
234 groupId = null;
235 else if (Int64.TryParse(groupClaim.Value, out long groupIdParsed))
237 else
238 {
239 tokenValidatedContext.Fail($"User has non-numeric '{groupIdClaimName}' claim!");
240 return;
241 }
242
245 .Groups
246 .AsQueryable()
247 .Where(group => group.Id == groupId.Value)
248 .Include(group => group.PermissionSet)
249 .FirstOrDefaultAsync(cancellationToken)
250 : null;
251
252 var missingClaimError = $"User missing '{groupIdClaimName}' claim!";
253 if (connection == default)
254 {
255 var username = principal.Identity?.Name;
256 if (username == null)
257 {
258 tokenValidatedContext.Fail("Failed to retrieve user's name from retrieved claims!");
259 return;
260 }
261
262 if (username.Contains(':', StringComparison.Ordinal))
263 {
264 tokenValidatedContext.Fail("Cannot create users with the ':' in their name!");
265 return;
266 }
267
268 if (group == null)
269 {
271 groupId.HasValue
272 ? $"'{groupIdClaimName}' does not point to a valid group!"
274 return;
275 }
276
277 logger.LogInformation("Registering new user '{name}' via OIDC scheme '{scheme}'", username, schemeKey);
278
280 .Users
281 .GetTgsUser(
282 dbUser => new User
283 {
284 Id = dbUser.Id!.Value,
285 },
287
288 user = new User
289 {
290 CreatedAt = DateTimeOffset.UtcNow,
291 CanonicalName = User.CanonicalizeName(username),
292 Name = username,
293 CreatedById = tgsUser.Id,
294 Enabled = true,
295 GroupId = group.Id,
296 OidcConnections = new List<OidcConnection>
297 {
298 new()
299 {
300 SchemeKey = schemeKey,
301 ExternalUserId = userId,
302 },
303 },
304 PasswordHash = "_", // This can't be hashed
305 };
306
308 }
309 else
310 {
311 user = connection.User!;
312
313 // group update
314 if (group == null)
315 {
316 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);
318 {
321 };
322 user.GroupId = null;
323 user.Enabled = false;
324
326 return;
327 }
328
329 logger.LogDebug("User {id} mapped to group {groupId} via OIDC login on scheme '{scheme}'", user.Id, groupId, schemeKey);
331 if (user.PermissionSet != null)
333
334 user.Enabled = true;
335 }
336
338 }
339
341
343 user,
344 expires,
345 Guid.NewGuid().ToString(),
346 null,
347 null);
348 }
349 }
350}
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.
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.