tgstation-server  4.4.0
The /tg/station 13 server suite
TokenFactory.cs
Go to the documentation of this file.
1 using Microsoft.IdentityModel.Tokens;
2 using System;
3 using System.Globalization;
4 using System.IdentityModel.Tokens.Jwt;
5 using System.Security.Claims;
6 using System.Threading;
7 using System.Threading.Tasks;
11 
12 namespace Tgstation.Server.Host.Security
13 {
15  sealed class TokenFactory : ITokenFactory
16  {
20  const uint TokenExpiryMinutes = 15;
21 
25  const uint TokenClockSkewMinutes = 1;
26 
30  const uint TokenSigningKeyByteAmount = 256;
31 
33  public TokenValidationParameters ValidationParameters { get; }
34 
39 
46  public TokenFactory(
47  IAsyncDelayer asyncDelayer,
48  ICryptographySuite cryptographySuite,
49  IAssemblyInformationProvider assemblyInformationProvider)
50  {
51  ValidationParameters = new TokenValidationParameters
52  {
53  ValidateIssuerSigningKey = true,
54  IssuerSigningKey = new SymmetricSecurityKey(cryptographySuite.GetSecureBytes(TokenSigningKeyByteAmount)),
55 
56  ValidateIssuer = true,
57  ValidIssuer = assemblyInformationProvider.Name.Name,
58 
59  ValidateLifetime = true,
60  ValidateAudience = true,
61  ValidAudience = typeof(Token).Assembly.GetName().Name,
62 
63  ClockSkew = TimeSpan.FromMinutes(TokenClockSkewMinutes),
64 
65  RequireSignedTokens = true,
66 
67  RequireExpirationTime = true
68  };
69 
70  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
71  }
72 
74  public async Task<Token> CreateToken(Models.User user, CancellationToken cancellationToken)
75  {
76  if (user == null)
77  throw new ArgumentNullException(nameof(user));
78 
79  var now = DateTimeOffset.Now;
80  var nowUnix = now.ToUnixTimeSeconds();
81 
82  // this prevents validation conflicts down the line
83  // tldr we can (theoretically) send a token the same second we receive it
84  // since unix time rounds down, it looks like it came from before the user changed their password
85  // this happens occasionally in unit tests
86  // just delay a second so we can force a round up
87  var lpuUnix = user.LastPasswordUpdate?.ToUnixTimeSeconds();
88  if (nowUnix == lpuUnix)
89  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
90 
91  var expiry = now.AddMinutes(TokenExpiryMinutes);
92  var claims = new Claim[]
93  {
94  new Claim(JwtRegisteredClaimNames.Sub, user.Id.Value.ToString(CultureInfo.InvariantCulture)),
95  new Claim(JwtRegisteredClaimNames.Exp, expiry.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)),
96  new Claim(JwtRegisteredClaimNames.Nbf, nowUnix.ToString(CultureInfo.InvariantCulture)),
97  new Claim(JwtRegisteredClaimNames.Iss, ValidationParameters.ValidIssuer),
98  new Claim(JwtRegisteredClaimNames.Aud, ValidationParameters.ValidAudience)
99  };
100 
101  var token = new JwtSecurityToken(new JwtHeader(new SigningCredentials(ValidationParameters.IssuerSigningKey, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));
102  return new Token
103  {
104  Bearer = new JwtSecurityTokenHandler().WriteToken(token),
105  ExpiresAt = expiry
106  };
107  }
108  }
109 }
byte[] GetSecureBytes(uint amount)
Generates a secure set of bytes
Contains various cryptographic functions
readonly IAsyncDelayer asyncDelayer
The IAsyncDelayer for the TokenFactory
Definition: TokenFactory.cs:38
Represents a JWT returned by the API
Definition: Token.cs:8
async Task< Token > CreateToken(Models.User user, CancellationToken cancellationToken)
Create a Token for a given user
Definition: TokenFactory.cs:74
TokenFactory(IAsyncDelayer asyncDelayer, ICryptographySuite cryptographySuite, IAssemblyInformationProvider assemblyInformationProvider)
Construct a TokenFactory
Definition: TokenFactory.cs:46