Files
EonaCat.SecureToken/EonaCat.SecureToken/Models/TokenPairDescriptor.cs
T

186 lines
7.2 KiB
C#

using System;
namespace EonaCat.SecureToken.Core
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
/// <summary>
/// Represents an access token and refresh token pair.
/// </summary>
public sealed class AccessTokenRefreshTokenPair
{
/// <summary>
/// The access token (short-lived, typically for API requests).
/// </summary>
public string AccessToken { get; set; }
/// <summary>
/// The refresh token (long-lived, used to obtain new access tokens).
/// </summary>
public string RefreshToken { get; set; }
/// <summary>
/// Creates a new token pair.
/// </summary>
public AccessTokenRefreshTokenPair(string accessToken, string refreshToken)
{
if (string.IsNullOrWhiteSpace(accessToken))
{
throw new ArgumentException("Access token cannot be null or empty.", nameof(accessToken));
}
if (string.IsNullOrWhiteSpace(refreshToken))
{
throw new ArgumentException("Refresh token cannot be null or empty.", nameof(refreshToken));
}
AccessToken = accessToken;
RefreshToken = refreshToken;
}
}
/// <summary>
/// Helper for issuing paired access and refresh tokens with optimal defaults.
/// </summary>
public static class TokenPairDescriptor
{
/// <summary>
/// Default lifetime for access tokens: 15 minutes.
/// </summary>
public static readonly TimeSpan DefaultAccessTokenLifetime = TimeSpan.FromMinutes(15);
/// <summary>
/// Default lifetime for refresh tokens: 30 days.
/// </summary>
public static readonly TimeSpan DefaultRefreshTokenLifetime = TimeSpan.FromDays(30);
/// <summary>
/// Creates an access token and refresh token pair with secure defaults.
/// </summary>
/// <param name="service">The token service</param>
/// <param name="subject">The subject (user ID, etc.)</param>
/// <param name="issuer">The issuer</param>
/// <param name="audience">The audience</param>
/// <param name="accessTokenLifetime">Custom access token lifetime; defaults to 15 minutes</param>
/// <param name="refreshTokenLifetime">Custom refresh token lifetime; defaults to 30 days</param>
/// <returns>A pair of access and refresh tokens</returns>
public static AccessTokenRefreshTokenPair CreatePair(
ITokenService service,
string subject,
string issuer,
string audience,
TimeSpan? accessTokenLifetime = null,
TimeSpan? refreshTokenLifetime = null)
{
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (string.IsNullOrWhiteSpace(subject))
{
throw new ArgumentException("Subject cannot be null or empty.", nameof(subject));
}
if (string.IsNullOrWhiteSpace(issuer))
{
throw new ArgumentException("Issuer cannot be null or empty.", nameof(issuer));
}
if (string.IsNullOrWhiteSpace(audience))
{
throw new ArgumentException("Audience cannot be null or empty.", nameof(audience));
}
var accessLifetime = accessTokenLifetime ?? DefaultAccessTokenLifetime;
var refreshLifetime = refreshTokenLifetime ?? DefaultRefreshTokenLifetime;
var accessToken = service.Issue(
TokenDescriptor.Create()
.ForSubject(subject)
.IssuedBy(issuer)
.ForAudience(audience)
.WithLifetime(accessLifetime));
var refreshToken = service.Issue(
TokenDescriptor.Create()
.ForSubject(subject)
.IssuedBy(issuer)
.ForAudience(audience)
.AsRefreshToken()
.WithLifetime(refreshLifetime));
return new AccessTokenRefreshTokenPair(accessToken, refreshToken);
}
/// <summary>
/// Creates an access token and refresh token pair with custom claims and roles.
/// </summary>
/// <param name="service">The token service</param>
/// <param name="subject">The subject (user ID, etc.)</param>
/// <param name="issuer">The issuer</param>
/// <param name="audience">The audience</param>
/// <param name="configure">Optional action to configure additional claims and roles</param>
/// <param name="accessTokenLifetime">Custom access token lifetime; defaults to 15 minutes</param>
/// <param name="refreshTokenLifetime">Custom refresh token lifetime; defaults to 30 days</param>
/// <returns>A pair of access and refresh tokens</returns>
public static AccessTokenRefreshTokenPair CreatePair(
ITokenService service,
string subject,
string issuer,
string audience,
Action<TokenDescriptor>? configure = null,
TimeSpan? accessTokenLifetime = null,
TimeSpan? refreshTokenLifetime = null)
{
if (service is null)
{
throw new ArgumentNullException(nameof(service));
}
if (string.IsNullOrWhiteSpace(subject))
{
throw new ArgumentException("Subject cannot be null or empty.", nameof(subject));
}
if (string.IsNullOrWhiteSpace(issuer))
{
throw new ArgumentException("Issuer cannot be null or empty.", nameof(issuer));
}
if (string.IsNullOrWhiteSpace(audience))
{
throw new ArgumentException("Audience cannot be null or empty.", nameof(audience));
}
var accessLifetime = accessTokenLifetime ?? DefaultAccessTokenLifetime;
var refreshLifetime = refreshTokenLifetime ?? DefaultRefreshTokenLifetime;
var baseAccessDescriptor = TokenDescriptor.Create()
.ForSubject(subject)
.IssuedBy(issuer)
.ForAudience(audience)
.WithLifetime(accessLifetime);
configure?.Invoke(baseAccessDescriptor);
var accessToken = service.Issue(baseAccessDescriptor);
// Refresh token uses same claims (roles, custom claims) but longer lifetime
var baseRefreshDescriptor = TokenDescriptor.Create()
.ForSubject(subject)
.IssuedBy(issuer)
.ForAudience(audience)
.AsRefreshToken()
.WithLifetime(refreshLifetime);
configure?.Invoke(baseRefreshDescriptor);
var refreshToken = service.Issue(baseRefreshDescriptor);
return new AccessTokenRefreshTokenPair(accessToken, refreshToken);
}
}
}