Skip to content

Commit ec457ca

Browse files
committed
feat(M13-003): Implement refresh tokens with rotation and revocation
Implements OAuth2 refresh tokens following RFC 6749 with security best practices: - RefreshToken entity with TokenFamilyId for chain tracking - Token rotation: old token revoked, new one issued on each use - Token reuse detection: entire family revoked on reuse attempt - Revocation endpoint following RFC 7009 - Audit logging for token issuance, rotation, and revocation events Key features: - SHA-256 hashed token storage - "octr_" prefix for easy identification - Configurable token lifetime (default: 30 days) - Support for both public and confidential clients - Scope preservation across token refresh
1 parent 8cfde2d commit ec457ca

File tree

11 files changed

+2743
-39
lines changed

11 files changed

+2743
-39
lines changed

src/Octopus.Server.App/Auth/OAuthTokenService.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,21 @@ string GenerateAccessToken(
6969
/// </summary>
7070
string GenerateAuthorizationCode();
7171

72+
/// <summary>
73+
/// Generates a cryptographically random refresh token.
74+
/// </summary>
75+
string GenerateRefreshToken();
76+
7277
/// <summary>
7378
/// Hashes an authorization code for storage.
7479
/// </summary>
7580
string HashCode(string code);
7681

82+
/// <summary>
83+
/// Hashes a refresh token for storage using SHA-256.
84+
/// </summary>
85+
string HashRefreshToken(string token);
86+
7787
/// <summary>
7888
/// Verifies a PKCE code verifier against a stored code challenge.
7989
/// </summary>
@@ -89,10 +99,25 @@ string GenerateAccessToken(
8999
/// </summary>
90100
int AccessTokenLifetimeSeconds { get; }
91101

102+
/// <summary>
103+
/// Gets the refresh token lifetime in seconds.
104+
/// </summary>
105+
int RefreshTokenLifetimeSeconds { get; }
106+
107+
/// <summary>
108+
/// Whether refresh tokens are enabled.
109+
/// </summary>
110+
bool RefreshTokensEnabled { get; }
111+
92112
/// <summary>
93113
/// Gets the authorization code expiration time from now.
94114
/// </summary>
95115
DateTimeOffset GetAuthorizationCodeExpiration();
116+
117+
/// <summary>
118+
/// Gets the refresh token expiration time from now.
119+
/// </summary>
120+
DateTimeOffset GetRefreshTokenExpiration();
96121
}
97122

98123
/// <summary>
@@ -120,6 +145,10 @@ public OAuthTokenService(IOptions<OAuthTokenOptions> options)
120145

121146
public int AccessTokenLifetimeSeconds => _options.AccessTokenLifetimeMinutes * 60;
122147

148+
public int RefreshTokenLifetimeSeconds => _options.RefreshTokenLifetimeDays * 24 * 60 * 60;
149+
150+
public bool RefreshTokensEnabled => _options.EnableRefreshTokens;
151+
123152
public string GenerateAccessToken(
124153
string subject,
125154
Guid userId,
@@ -164,6 +193,29 @@ public string GenerateAuthorizationCode()
164193
.TrimEnd('=');
165194
}
166195

196+
public string GenerateRefreshToken()
197+
{
198+
// Generate a 256-bit random refresh token with "octr_" prefix for identification
199+
var bytes = new byte[32];
200+
using var rng = RandomNumberGenerator.Create();
201+
rng.GetBytes(bytes);
202+
var token = Convert.ToBase64String(bytes)
203+
.Replace("+", "-")
204+
.Replace("/", "_")
205+
.TrimEnd('=');
206+
return $"octr_{token}";
207+
}
208+
209+
public string HashRefreshToken(string token)
210+
{
211+
// Use SHA-256 for hashing refresh tokens
212+
// Refresh tokens are long-lived but single-use per rotation
213+
using var sha256 = SHA256.Create();
214+
var bytes = Encoding.UTF8.GetBytes(token);
215+
var hash = sha256.ComputeHash(bytes);
216+
return Convert.ToBase64String(hash);
217+
}
218+
167219
public string HashCode(string code)
168220
{
169221
// Use SHA-256 for hashing authorization codes
@@ -243,4 +295,9 @@ public DateTimeOffset GetAuthorizationCodeExpiration()
243295
{
244296
return DateTimeOffset.UtcNow.AddMinutes(_options.AuthorizationCodeLifetimeMinutes);
245297
}
298+
299+
public DateTimeOffset GetRefreshTokenExpiration()
300+
{
301+
return DateTimeOffset.UtcNow.AddDays(_options.RefreshTokenLifetimeDays);
302+
}
246303
}

0 commit comments

Comments
 (0)