Files
EonaCat.SecureToken/EonaCat.SecureToken/Models/TokenDescriptor.cs
T
2026-06-19 16:34:50 +02:00

132 lines
4.0 KiB
C#

using System;
using System.Collections.Generic;
namespace 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>
/// Fluent builder for constructing token claims before issuing.
/// </summary>
public sealed class TokenDescriptor
{
private string _subject = string.Empty;
private string _issuer = string.Empty;
private readonly List<string> _audiences = new List<string>();
private readonly List<string> _roles = new List<string>();
private readonly Dictionary<string, string> _custom = new Dictionary<string, string>();
private TimeSpan _lifetime = TimeSpan.FromHours(1);
private TimeSpan _notBeforeDelay = TimeSpan.Zero;
private string? _bindingContext;
private string _tokenType = TokenTypeConstants.Access;
public static TokenDescriptor Create() => new TokenDescriptor();
public TokenDescriptor ForSubject(string subject)
{
_subject = subject;
return this;
}
public TokenDescriptor IssuedBy(string issuer)
{
_issuer = issuer;
return this;
}
public TokenDescriptor ForAudience(string audience)
{
_audiences.Add(audience);
return this;
}
public TokenDescriptor ForAudiences(IEnumerable<string> audiences)
{
_audiences.AddRange(audiences);
return this;
}
public TokenDescriptor WithRole(string role)
{
_roles.Add(role);
return this;
}
public TokenDescriptor WithRoles(IEnumerable<string> roles)
{
_roles.AddRange(roles);
return this;
}
public TokenDescriptor WithClaim(string key, string value)
{
_custom[key] = value;
return this;
}
public TokenDescriptor WithLifetime(TimeSpan lifetime)
{
if (lifetime <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(lifetime), "Lifetime must be positive.");
}
_lifetime = lifetime;
return this;
}
public TokenDescriptor ExpiresIn(int minutes) =>
WithLifetime(TimeSpan.FromMinutes(minutes));
/// <summary>
/// Bind this token to a specific context (IP address, device fingerprint, etc.).
/// The same binding must be provided during validation.
/// </summary>
public TokenDescriptor BoundTo(string context)
{
_bindingContext = context;
return this;
}
/// <summary>
/// Tag the token type to prevent cross-purpose usage.
/// Use <see cref="TokenTypeConstants"/> for well-known values.
/// </summary>
public TokenDescriptor OfType(string tokenType)
{
_tokenType = tokenType;
return this;
}
public TokenDescriptor AsRefreshToken() => OfType(TokenTypeConstants.Refresh);
public TokenDescriptor AsServiceAccount() => OfType(TokenTypeConstants.ServiceAccount);
public TokenDescriptor NotValidBefore(TimeSpan delay)
{
_notBeforeDelay = delay;
return this;
}
internal TokenClaims Build(int keyGeneration = 0)
{
var now = DateTimeOffset.UtcNow;
return new TokenClaims
{
Subject = _subject,
Issuer = _issuer,
Audiences = _audiences.AsReadOnly(),
Roles = _roles.AsReadOnly(),
Custom = _custom,
IssuedAt = now,
NotBefore = now + _notBeforeDelay,
ExpiresAt = now + _lifetime,
BindingContext = _bindingContext,
TokenType = _tokenType,
KeyGeneration = keyGeneration,
};
}
}
}