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 .Where(x => x.Id == userId)
133 .Include(x => x.CreatedBy)
134 .Include(x => x.PermissionSet)
135 .Include(x => x.Group)
136 .ThenInclude(x => x!.PermissionSet)
137 .Include(x => x.OAuthConnections)
138 .Include(x => x.OidcConnections)
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 {
169 .Where(x => x.PermissionSetId == userPermissionSet!.Id && x.InstanceId == instanceId && x.Instance!.SwarmIdentifer == swarmConfigurationOptions.Value.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 .Where(oidcConnection => oidcConnection.ExternalUserId == userId && oidcConnection.SchemeKey == deprefixedScheme)
212 .Include(oidcConnection => oidcConnection.User)
213 .ThenInclude(user => user!.Group)
214 .ThenInclude(group => group!.PermissionSet)
215 .FirstOrDefaultAsync(cancellationToken);
216
217 User user;
218 if (!securityConfigurationOptions.Value.OidcStrictMode)
219 {
220 if (connection == default)
221 {
222 tokenValidatedContext.Fail($"Unable to find user with OidcConnection for {deprefixedScheme}/{userId}!");
223 return;
224 }
225
226 user = connection.User!;
227 }
228 else
229 {
231 long? groupId;
232 if (groupClaim == default)
233 groupId = null;
234 else if (Int64.TryParse(groupClaim.Value, out long groupIdParsed))
236 else
237 {
238 tokenValidatedContext.Fail($"User has non-numeric '{groupIdClaimName}' claim!");
239 return;
240 }
241
244 .Groups
245 .Where(group => group.Id == groupId.Value)
246 .Include(group => group.PermissionSet)
247 .FirstOrDefaultAsync(cancellationToken)
248 : null;
249
250 var missingClaimError = $"User missing '{groupIdClaimName}' claim!";
251 if (connection == default)
252 {
253 var username = principal.Identity?.Name;
254 if (username == null)
255 {
256 tokenValidatedContext.Fail("Failed to retrieve user's name from retrieved claims!");
257 return;
258 }
259
260 if (username.Contains(':', StringComparison.Ordinal))
261 {
262 tokenValidatedContext.Fail("Cannot create users with the ':' in their name!");
263 return;
264 }
265
266 if (group == null)
267 {
269 groupId.HasValue
270 ? $"'{groupIdClaimName}' does not point to a valid group!"
272 return;
273 }
274
275 logger.LogInformation("Registering new user '{name}' via OIDC scheme '{scheme}'", username, schemeKey);
276
278 .Users
279 .GetTgsUser(
280 dbUser => new User
281 {
282 Id = dbUser.Id!.Value,
283 },
284 cancellationToken);
285
286 user = new User
287 {
288 CreatedAt = DateTimeOffset.UtcNow,
289 CanonicalName = User.CanonicalizeName(username),
290 Name = username,
291 CreatedById = tgsUser.Id,
292 Enabled = true,
293 GroupId = group.Id,
294 OidcConnections = new List<OidcConnection>
295 {
296 new()
297 {
298 SchemeKey = schemeKey,
299 ExternalUserId = userId,
300 },
301 },
302 PasswordHash = "_", // This can't be hashed
303 };
304
306 }
307 else
308 {
309 user = connection.User!;
310
311 // group update
312 if (group == null)
313 {
314 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);
316 {
319 };
320 user.GroupId = null;
321 user.Enabled = false;
322
324 return;
325 }
326
327 logger.LogDebug("User {id} mapped to group {groupId} via OIDC login on scheme '{scheme}'", user.Id, groupId, schemeKey);
329 if (user.PermissionSet != null)
331
332 user.Enabled = true;
333 }
334
335 await databaseContext.Save(cancellationToken);
336 }
337
339
341 user,
342 expires,
343 Guid.NewGuid().ToString(),
344 null,
345 null);
346 }
347 }
348}
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
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
readonly IOptionsSnapshot< SecurityConfiguration > securityConfigurationOptions
The IOptionsSnapshot<TOptions> of 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 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.
AuthenticationContextFactory(IDatabaseContext databaseContext, IIdentityCache identityCache, IApiHeadersProvider apiHeadersProvider, IOptions< SwarmConfiguration > swarmConfigurationOptions, IOptionsSnapshot< SecurityConfiguration > securityConfigurationOptions, ILogger< AuthenticationContextFactory > logger)
Initializes a new instance of the AuthenticationContextFactory class.
readonly IOptions< SwarmConfiguration > swarmConfigurationOptions
The IOptions<TOptions> of SwarmConfiguration for the AuthenticationContextFactory.
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< InstancePermissionSet > InstancePermissionSets
The InstancePermissionSets in the IDatabaseContext.
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.