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 .FirstOrDefaultAsync(cancellationToken);
139 if (user == default)
140 {
141 tokenValidatedContext.Fail($"Unable to find user with ID {userId}!");
142 return;
143 }
144
146 if (user.SystemIdentifier != null)
148 else
149 {
150 if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate >= notBefore)
151 {
152 tokenValidatedContext.Fail($"Rejecting token for user {userId} created before last modification: {user.LastPasswordUpdate.Value}");
153 return;
154 }
155
156 systemIdentity = null;
157 }
158
159 var userPermissionSet = user.PermissionSet ?? user.Group!.PermissionSet;
160 try
161 {
164 if (instanceId.HasValue)
165 {
168 .Where(x => x.PermissionSetId == userPermissionSet!.Id && x.InstanceId == instanceId && x.Instance!.SwarmIdentifer == swarmConfigurationOptions.Value.Identifier)
169 .Include(x => x.Instance)
170 .FirstOrDefaultAsync(cancellationToken);
171
172 if (instancePermissionSet == null)
173 logger.LogDebug("User {userId} does not have permissions on instance {instanceId}!", userId, instanceId.Value);
174 }
175
177 user,
178 expires,
179 jwt.EncodedSignature, // signature is enough to uniquely identify the session as it is composite of all the inputs
182 }
183 catch
184 {
185 systemIdentity?.Dispose();
186 throw;
187 }
188 }
189
191#pragma warning disable CA1506 // TODO: Decomplexify
193#pragma warning restore CA1506
194 {
196
198 if (principal == null)
199 throw new InvalidOperationException("Expected a valid principal here!");
200
202 if (userIdClaim == default)
203 throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!");
204
205 var userId = userIdClaim.Value;
206 var scheme = tokenValidatedContext.Scheme.Name;
210 .Where(oidcConnection => oidcConnection.ExternalUserId == userId && oidcConnection.SchemeKey == deprefixedScheme)
211 .Include(oidcConnection => oidcConnection.User)
212 .ThenInclude(user => user!.Group)
213 .ThenInclude(group => group!.PermissionSet)
214 .FirstOrDefaultAsync(cancellationToken);
215
216 User user;
217 if (!securityConfigurationOptions.Value.OidcStrictMode)
218 {
219 if (connection == default)
220 {
221 tokenValidatedContext.Fail($"Unable to find user with OidcConnection for {deprefixedScheme}/{userId}!");
222 return;
223 }
224
225 user = connection.User!;
226 }
227 else
228 {
230 long? groupId;
231 if (groupClaim == default)
232 groupId = null;
233 else if (Int64.TryParse(groupClaim.Value, out long groupIdParsed))
235 else
236 {
237 tokenValidatedContext.Fail($"User has non-numeric '{groupIdClaimName}' claim!");
238 return;
239 }
240
243 .Groups
244 .Where(group => group.Id == groupId.Value)
245 .Include(group => group.PermissionSet)
246 .FirstOrDefaultAsync(cancellationToken)
247 : null;
248
249 var missingClaimError = $"User missing '{groupIdClaimName}' claim!";
250 if (connection == default)
251 {
252 var username = principal.Identity?.Name;
253 if (username == null)
254 {
255 tokenValidatedContext.Fail("Failed to retrieve user's name from retrieved claims!");
256 return;
257 }
258
259 if (username.Contains(':', StringComparison.Ordinal))
260 {
261 tokenValidatedContext.Fail("Cannot create users with the ':' in their name!");
262 return;
263 }
264
265 if (group == null)
266 {
268 groupId.HasValue
269 ? $"'{groupIdClaimName}' does not point to a valid group!"
271 return;
272 }
273
274 logger.LogInformation("Registering new user '{name}' via OIDC scheme '{scheme}'", username, schemeKey);
275
277 .Users
278 .GetTgsUser(
279 dbUser => new User
280 {
281 Id = dbUser.Id!.Value,
282 },
283 cancellationToken);
284
285 user = new User
286 {
287 CreatedAt = DateTimeOffset.UtcNow,
288 CanonicalName = User.CanonicalizeName(username),
289 Name = username,
290 CreatedById = tgsUser.Id,
291 Enabled = true,
292 GroupId = group.Id,
293 OidcConnections = new List<OidcConnection>
294 {
295 new()
296 {
297 SchemeKey = schemeKey,
298 ExternalUserId = userId,
299 },
300 },
301 PasswordHash = "_", // This can't be hashed
302 };
303
305 }
306 else
307 {
308 user = connection.User!;
309
310 // group update
311 if (group == null)
312 {
313 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);
315 {
318 };
319 user.GroupId = null;
320 user.Enabled = false;
321
323 return;
324 }
325
326 logger.LogDebug("User {id} mapped to group {groupId} via OIDC login on scheme '{scheme}'", user.Id, groupId, schemeKey);
328 if (user.PermissionSet != null)
330
331 user.Enabled = true;
332 }
333
334 await databaseContext.Save(cancellationToken);
335 }
336
338
340 user,
341 expires,
342 Guid.NewGuid().ToString(),
343 null,
344 null);
345 }
346 }
347}
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.