tgstation-server
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.Reflection;
6 using System.Security.Claims;
7 using System.Threading;
8 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 
45  public TokenFactory(IAsyncDelayer asyncDelayer, ICryptographySuite cryptographySuite)
46  {
47  ValidationParameters = new TokenValidationParameters
48  {
49  ValidateIssuerSigningKey = true,
50  IssuerSigningKey = new SymmetricSecurityKey(cryptographySuite.GetSecureBytes(TokenSigningKeyByteAmount)),
51 
52  ValidateIssuer = true,
53  ValidIssuer = Assembly.GetExecutingAssembly().GetName().Name,
54 
55  ValidateLifetime = true,
56  ValidateAudience = true,
57  ValidAudience = typeof(Token).Assembly.GetName().Name,
58 
59  ClockSkew = TimeSpan.FromMinutes(TokenClockSkewMinutes),
60 
61  RequireSignedTokens = true,
62 
63  RequireExpirationTime = true
64  };
65 
66  this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
67  }
68 
70  public async Task<Token> CreateToken(Models.User user, CancellationToken cancellationToken)
71  {
72  if (user == null)
73  throw new ArgumentNullException(nameof(user));
74 
75  var now = DateTimeOffset.Now;
76 
77  var nowUnix = now.ToUnixTimeSeconds();
78  //this prevents validation conflicts down the line
79  //tldr we can (theoretically) send a token the same second we receive it
80  //since unix time rounds down, it looks like it came from before the user changed their password
81  //this happens occasionally in unit tests
82  //just delay a second so we can force a round up
83  var lpuUnix = user.LastPasswordUpdate?.ToUnixTimeSeconds();
84  if (nowUnix == lpuUnix)
85  await asyncDelayer.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
86 
87  var expiry = now.AddMinutes(TokenExpiryMinutes);
88  var claims = new Claim[]
89  {
90  new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString(CultureInfo.InvariantCulture)),
91  new Claim(JwtRegisteredClaimNames.Exp, expiry.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture)),
92  new Claim(JwtRegisteredClaimNames.Nbf, nowUnix.ToString(CultureInfo.InvariantCulture)),
93  new Claim(JwtRegisteredClaimNames.Iss, ValidationParameters.ValidIssuer),
94  new Claim(JwtRegisteredClaimNames.Aud, ValidationParameters.ValidAudience)
95  };
96 
97  var token = new JwtSecurityToken(new JwtHeader(new SigningCredentials(ValidationParameters.IssuerSigningKey, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));
98  return new Token { Bearer = new JwtSecurityTokenHandler().WriteToken(token), ExpiresAt = expiry };
99  }
100  }
101 }
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
TokenFactory(IAsyncDelayer asyncDelayer, ICryptographySuite cryptographySuite)
Construct a TokenFactory
Definition: TokenFactory.cs:45
async Task< Token > CreateToken(Models.User user, CancellationToken cancellationToken)
Create a Token for a given user
Definition: TokenFactory.cs:70