tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
AuthenticationContextFactory.cs
Go to the documentation of this file.
1using System;
2using System.Globalization;
3using System.Linq;
4using System.Security.Claims;
5using System.Threading;
6using System.Threading.Tasks;
7
8using Microsoft.AspNetCore.Authentication.JwtBearer;
9using Microsoft.EntityFrameworkCore;
10using Microsoft.Extensions.Logging;
11using Microsoft.Extensions.Options;
12using Microsoft.IdentityModel.JsonWebTokens;
13using Microsoft.IdentityModel.Tokens;
14
20
22{
25 {
30
35
40
44 readonly ILogger<AuthenticationContextFactory> logger;
45
50
55
60
65
77 IApiHeadersProvider apiHeadersProvider,
78 IOptions<SwarmConfiguration> swarmConfigurationOptions,
79 ILogger<AuthenticationContextFactory> logger)
80 {
81 this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext));
82 this.identityCache = identityCache ?? throw new ArgumentNullException(nameof(identityCache));
83 ArgumentNullException.ThrowIfNull(apiHeadersProvider);
84
85 apiHeaders = apiHeadersProvider.ApiHeaders;
86 swarmConfiguration = swarmConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(swarmConfigurationOptions));
87 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
88
90 }
91
94
96 #pragma warning disable CA1506 // TODO: Decomplexify
97 public async Task ValidateToken(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
98 #pragma warning restore CA1506
99 {
100 ArgumentNullException.ThrowIfNull(tokenValidatedContext);
101
102 if (tokenValidatedContext.SecurityToken is not JsonWebToken jwt)
103 throw new ArgumentException($"Expected {nameof(tokenValidatedContext)} to contain a {nameof(JsonWebToken)}!", nameof(tokenValidatedContext));
104
105 if (Interlocked.Exchange(ref initialized, 1) != 0)
106 throw new InvalidOperationException("Authentication context has already been loaded");
107
108 var principal = new ClaimsPrincipal(new ClaimsIdentity(jwt.Claims));
109
110 var userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub);
111 if (userIdClaim == default)
112 throw new InvalidOperationException($"Missing '{JwtRegisteredClaimNames.Sub}' claim!");
113
114 long userId;
115 try
116 {
117 userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture);
118 }
119 catch (Exception e)
120 {
121 throw new InvalidOperationException("Failed to parse user ID!", e);
122 }
123
124 DateTimeOffset ParseTime(string key)
125 {
126 var claim = principal.FindFirst(key);
127 if (claim == default)
128 throw new InvalidOperationException($"Missing '{key}' claim!");
129
130 try
131 {
132 return new DateTimeOffset(
133 EpochTime.DateTime(
134 Int64.Parse(claim.Value, CultureInfo.InvariantCulture)));
135 }
136 catch (Exception ex)
137 {
138 throw new InvalidOperationException($"Failed to parse '{key}'!", ex);
139 }
140 }
141
142 var notBefore = ParseTime(JwtRegisteredClaimNames.Nbf);
143 var expires = ParseTime(JwtRegisteredClaimNames.Exp);
144
145 var user = await databaseContext
146 .Users
147 .AsQueryable()
148 .Where(x => x.Id == userId)
149 .Include(x => x.CreatedBy)
150 .Include(x => x.PermissionSet)
151 .Include(x => x.Group)
152 .ThenInclude(x => x!.PermissionSet)
153 .Include(x => x.OAuthConnections)
154 .FirstOrDefaultAsync(cancellationToken);
155 if (user == default)
156 {
157 tokenValidatedContext.Fail($"Unable to find user with ID {userId}!");
158 return;
159 }
160
161 ISystemIdentity? systemIdentity;
162 if (user.SystemIdentifier != null)
163 systemIdentity = identityCache.LoadCachedIdentity(user);
164 else
165 {
166 if (user.LastPasswordUpdate.HasValue && user.LastPasswordUpdate >= notBefore)
167 {
168 tokenValidatedContext.Fail($"Rejecting token for user {userId} created before last modification: {user.LastPasswordUpdate.Value}");
169 return;
170 }
171
172 systemIdentity = null;
173 }
174
175 var userPermissionSet = user.PermissionSet ?? user.Group!.PermissionSet;
176 try
177 {
178 InstancePermissionSet? instancePermissionSet = null;
179 var instanceId = apiHeaders?.InstanceId;
180 if (instanceId.HasValue)
181 {
182 instancePermissionSet = await databaseContext.InstancePermissionSets
183 .AsQueryable()
184 .Where(x => x.PermissionSetId == userPermissionSet!.Id && x.InstanceId == instanceId && x.Instance!.SwarmIdentifer == swarmConfiguration.Identifier)
185 .Include(x => x.Instance)
186 .FirstOrDefaultAsync(cancellationToken);
187
188 if (instancePermissionSet == null)
189 logger.LogDebug("User {userId} does not have permissions on instance {instanceId}!", userId, instanceId.Value);
190 }
191
193 user,
194 expires,
195 jwt.EncodedSignature, // signature is enough to uniquely identify the session as it is composite of all the inputs
196 instancePermissionSet,
197 systemIdentity);
198 }
199 catch
200 {
201 systemIdentity?.Dispose();
202 throw;
203 }
204 }
205 }
206}
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
string? Identifier
The server's identifier.
Configuration for the server swarm system.
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.
readonly AuthenticationContext currentAuthenticationContext
Backing field for CurrentAuthenticationContext.
async Task ValidateToken(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken)
Handles tokenValidatedContext .A Task representing the running operation.
AuthenticationContextFactory(IDatabaseContext databaseContext, IIdentityCache identityCache, IApiHeadersProvider apiHeadersProvider, IOptions< SwarmConfiguration > swarmConfigurationOptions, ILogger< AuthenticationContextFactory > logger)
Initializes a new instance of the AuthenticationContextFactory class.
void Initialize(User user, DateTimeOffset sessionExpiry, string sessionId, InstancePermissionSet? instanceUser, ISystemIdentity? systemIdentity)
Initializes the AuthenticationContext.
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.
ApiHeaders? ApiHeaders
The created Api.ApiHeaders, if any.