Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ public static class OpenIdConnectTokenManagementDefaults
/// </summary>
public const string ClientCredentialsClientNamePrefix = "Duende.TokenManagement.SchemeBasedClient:";

/// <summary>
/// Converts an authentication scheme to the synthetic client-credentials client name
/// used internally for per-scheme token handling.
/// </summary>
/// <param name="scheme">The authentication scheme.</param>
/// <returns>The derived client name.</returns>
public static ClientCredentialsClientName ToClientName(this Scheme scheme) =>
ClientCredentialsClientName.Parse(ClientCredentialsClientNamePrefix + scheme);
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ public TokenForParameters(UserRefreshToken refreshToken)
/// </summary>
public UserRefreshToken? RefreshToken { get; }

/// <summary>
/// Indicates whether no refresh token exists.
/// </summary>
/// <remarks>
/// When <see langword="true"/>, <see cref="TokenForSpecifiedParameters"/> is populated.
/// When <see langword="false"/>, <see cref="RefreshToken"/> is populated.
/// </remarks>
[MemberNotNullWhen(true, nameof(TokenForSpecifiedParameters))]
[MemberNotNullWhen(false, nameof(RefreshToken))]
public bool NoRefreshToken { get; private set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public sealed record UserToken : AccessTokenRequestHandler.IToken
/// </summary>
public required AccessToken AccessToken { get; init; }

/// <summary>
/// The DPoP proof key associated with this token when the token is DPoP-bound.
/// </summary>
Comment thread
Copilot marked this conversation as resolved.
public DPoPProofKey? DPoPJsonWebKey { get; init; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,15 @@ namespace Duende.AccessTokenManagement;
[JsonConverter(typeof(StringValueJsonConverter<AccessToken>))]
public readonly record struct AccessToken : IStronglyTypedValue<AccessToken>
{
/// <summary>
/// Returns the wrapped access token string.
/// </summary>
public override string ToString() => Value;

// Officially, there's no max length for JWTs, but 32k is a good limit
/// <summary>
/// The maximum supported length for an access token string.
/// </summary>
// Officially, there's no max length for JWTs, but 32k is a good limit.
public const int MaxLength = 32 * 1024; // 32k

private static readonly ValidationRule<string>[] Validators = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ public sealed class AccessTokenRequestHandler(
ILogger<AccessTokenRequestHandler> logger)
: DelegatingHandler
{
/// <inheritdoc />
protected override HttpResponseMessage Send(HttpRequestMessage request, CT ct) =>
throw new NotSupportedException(
"The (synchronous) Send() method is not supported. Please use the async SendAsync variant. ");

/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CT ct)
{
Expand Down Expand Up @@ -144,6 +146,9 @@ Task<TokenResult<IToken>> GetTokenAsync(
/// </summary>
public interface IToken
{
/// <summary>
/// The access token value to send.
/// </summary>
AccessToken AccessToken { get; }

/// <summary>
Expand All @@ -156,6 +161,9 @@ public interface IToken
/// </summary>
ClientId ClientId { get; }

/// <summary>
/// The HTTP authorization scheme for the access token.
/// </summary>
AccessTokenType? AccessTokenType { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ namespace Duende.AccessTokenManagement;
/// </summary>
public readonly record struct ClientCredentialsCacheKey : IStronglyTypedValue<ClientCredentialsCacheKey>
{
/// <summary>
/// Returns the wrapped cache key string.
/// </summary>
public override string ToString() => Value;

/// <summary>
/// The maximum supported length for a cache key value.
/// </summary>
public const int MaxLength = 1024;

private static readonly ValidationRule<string>[] Validators = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ namespace Duende.AccessTokenManagement;
/// <param name="value"></param>
public static implicit operator string(ClientCredentialsClientName value) => value.ToString();

/// <summary>
/// Returns the wrapped client name string.
/// </summary>
public override string ToString() => Value;

private static readonly ValidationRule<string>[] Validators = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ namespace Duende.AccessTokenManagement;
/// </summary>
public sealed class ClientCredentialsTokenManagementBuilder(IServiceCollection services)
{
/// <summary>
/// The service collection being configured.
/// </summary>
public IServiceCollection Services { get; } = services;

/// <summary>
/// Adds a client credentials client to the token management system
/// </summary>
/// <param name="name"></param>
/// <param name="configureOptions"></param>
/// <returns></returns>
/// <param name="name">The logical name of the client configuration.</param>
/// <param name="configureOptions">A delegate that configures the named <see cref="ClientCredentialsClient"/> options.</param>
/// <returns>The same builder instance for chaining.</returns>
public ClientCredentialsTokenManagementBuilder AddClient(string name,
Action<ClientCredentialsClient> configureOptions)
{
Expand Down
20 changes: 19 additions & 1 deletion access-token-management/src/AccessTokenManagement/ClientId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,40 @@ namespace Duende.AccessTokenManagement;
/// <param name="value"></param>
public static implicit operator string(ClientId value) => value.ToString();

/// <summary>
/// Returns the wrapped client identifier string.
/// </summary>
public override string ToString() => Value;

private static readonly ValidationRule<string>[] Validators = [
ValidationRules.MaxLength(1024)
];


/// <summary>
/// Prevents creating an uninitialized <see cref="ClientId"/> instance.
/// </summary>
/// <exception cref="InvalidOperationException">Always thrown.</exception>
public ClientId() => throw new InvalidOperationException("Can't create null value");
private ClientId(string value) => Value = value;

private string Value { get; }

/// <summary>
/// Attempts to parse and validate a client identifier string.
/// </summary>
/// <param name="value">The value to parse.</param>
/// <param name="parsed">The parsed <see cref="ClientId"/> when parsing succeeds.</param>
/// <param name="errors">Validation errors when parsing fails.</param>
/// <returns><see langword="true"/> when parsing succeeds; otherwise <see langword="false"/>.</returns>
public static bool TryParse(string value, [NotNullWhen(true)] out ClientId? parsed, out string[] errors) =>
IStronglyTypedValue<ClientId>.TryBuildValidatedObject(value, Validators, out parsed, out errors);

static ClientId IStronglyTypedValue<ClientId>.Create(string result) => new(result);

/// <summary>
/// Parses and validates a client identifier string.
/// </summary>
/// <param name="value">The value to parse.</param>
/// <returns>The parsed <see cref="ClientId"/>.</returns>
public static ClientId Parse(string value) => StringParsers<ClientId>.Parse(value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

namespace Duende.AccessTokenManagement;

/// <summary>
/// Represents an OAuth/OIDC client secret value.
/// </summary>
[TypeConverter(typeof(StringValueConverter<ClientSecret>))]
public readonly record struct ClientSecret : IStronglyTypedValue<ClientSecret>
{
Expand All @@ -16,6 +19,9 @@ namespace Duende.AccessTokenManagement;
/// <param name="value"></param>
public static implicit operator string(ClientSecret value) => value.ToString();

/// <summary>
/// Returns the wrapped client secret string.
/// </summary>
public override string ToString() => Value;

private static readonly ValidationRule<string>[] Validators = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,18 @@ public static class DPoPExtensions
private static readonly HttpRequestOptionsKey<bool> ForceRenewalOptionsKey = new("Duende.AccessTokenManagement.ForceRenewal");
private static readonly HttpRequestOptionsKey<DPoPNonce> DPoPNonceOptionsKey = new("Duende.AccessTokenManagement.DPoPNonce");

/// <summary>
/// Sets whether this request should force token renewal.
/// </summary>
/// <param name="request">The request to annotate.</param>
/// <param name="forceTokenRenewal"><see langword="true"/> to force renewal; otherwise <see langword="false"/>.</param>
public static void SetForceRenewal(this HttpRequestMessage request, bool forceTokenRenewal) => request.Options.Set(ForceRenewalOptionsKey, forceTokenRenewal);

/// <summary>
/// Gets whether this request is marked to force token renewal.
/// </summary>
/// <param name="request">The request to inspect.</param>
/// <returns><see langword="true"/> if force-renewal is enabled; otherwise <see langword="false"/>.</returns>
public static bool GetForceRenewal(this HttpRequestMessage request)
{
if (request.Options.TryGetValue(ForceRenewalOptionsKey, out var forceRenewal))
Expand All @@ -25,6 +35,11 @@ public static bool GetForceRenewal(this HttpRequestMessage request)
return false;
}

/// <summary>
/// Gets the DPoP nonce associated with this request, if one was set.
/// </summary>
/// <param name="request">The request to inspect.</param>
/// <returns>The nonce value, or <see langword="null"/> when none is present.</returns>
public static DPoPNonce? GetDPoPNonce(this HttpRequestMessage request)
{
if (request.Options.TryGetValue(DPoPNonceOptionsKey, out var nonce))
Expand All @@ -33,6 +48,12 @@ public static bool GetForceRenewal(this HttpRequestMessage request)
}
return null;
}

/// <summary>
/// Sets the DPoP nonce to use when generating a proof for this request.
/// </summary>
/// <param name="request">The request to annotate.</param>
/// <param name="nonce">The nonce value.</param>
public static void SetDPoPNonce(this HttpRequestMessage request, DPoPNonce nonce) => request.Options.Set(DPoPNonceOptionsKey, nonce);

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ namespace Duende.AccessTokenManagement.DPoP;
/// </summary>
public readonly record struct DPoPNonce : IStronglyTypedValue<DPoPNonce>
{
/// <summary>
/// Returns the wrapped nonce string.
/// </summary>
public override string ToString() => Value;

private static readonly ValidationRule<string>[] Validators =
Expand All @@ -24,18 +27,40 @@ namespace Duende.AccessTokenManagement.DPoP;
ValidationRules.MaxLength(4 * 1024),
];

/// <summary>
/// Prevents creating an uninitialized <see cref="DPoPNonce"/> instance.
/// </summary>
/// <exception cref="InvalidOperationException">Always thrown.</exception>
public DPoPNonce() => throw new InvalidOperationException("Can't create null value");

private DPoPNonce(string value) => Value = value;

private string Value { get; }

/// <summary>
/// Attempts to parse and validate a nonce string.
/// </summary>
/// <param name="value">The nonce string to parse.</param>
/// <param name="parsed">The parsed <see cref="DPoPNonce"/> when parsing succeeds.</param>
/// <param name="errors">Validation errors when parsing fails.</param>
/// <returns><see langword="true"/> when parsing succeeds; otherwise <see langword="false"/>.</returns>
public static bool TryParse(string value, [NotNullWhen(true)] out DPoPNonce? parsed, out string[] errors) =>
IStronglyTypedValue<DPoPNonce>.TryBuildValidatedObject(value, Validators, out parsed, out errors);


static DPoPNonce IStronglyTypedValue<DPoPNonce>.Create(string result) => new(result);

/// <summary>
/// Parses and validates a nonce string.
/// </summary>
/// <param name="value">The nonce string to parse.</param>
/// <returns>The parsed <see cref="DPoPNonce"/>.</returns>
public static DPoPNonce Parse(string value) => StringParsers<DPoPNonce>.Parse(value);

/// <summary>
/// Parses and validates a nonce string, returning <see langword="null"/> when the input is null or whitespace.
/// </summary>
/// <param name="value">The nonce string to parse.</param>
/// <returns>The parsed <see cref="DPoPNonce"/>, or <see langword="null"/>.</returns>
public static DPoPNonce? ParseOrDefault(string? value) => StringParsers<DPoPNonce>.ParseOrDefault(value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
namespace Duende.AccessTokenManagement.DPoP;

/// <summary>
/// The context for a DPoP nonce.
/// Identifies the request target for storing and retrieving DPoP nonces.
/// </summary>
public sealed record DPoPNonceContext
{
/// <summary>
/// The HTTP URL of the request
/// The HTTP URL of the request.
/// </summary>
public required Uri Url { get; set; }

/// <summary>
/// The HTTP method of the request
/// The HTTP method of the request.
/// </summary>
public required HttpMethod Method { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
namespace Duende.AccessTokenManagement.DPoP;

/// <summary>
/// Represents a strongly-typed DPoP proof value.
/// Represents a single signed DPoP proof JWT value.
/// </summary>
/// <remarks>
/// This value is sent in the HTTP <c>DPoP</c> header for one request.
/// It is generated from a <see cref="DPoPProofRequest"/> by <see cref="IDPoPProofService"/>
/// using a <see cref="DPoPProofKey"/>.
/// </remarks>
public readonly record struct DPoPProof : IStronglyTypedValue<DPoPProof>
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,28 @@

namespace Duende.AccessTokenManagement.DPoP;

/// <summary>
/// Represents the JSON Web Key (JWK) used to sign <see cref="DPoPProof"/> values.
/// </summary>
/// <remarks>
/// A proof key is the long-lived key material that binds issued tokens to a client.
/// A per-request <see cref="DPoPProof"/> is generated from this key by <see cref="IDPoPProofService"/>.
/// </remarks>
[TypeConverter(typeof(StringValueConverter<DPoPProofKey>))]
[JsonConverter(typeof(StringValueJsonConverter<DPoPProofKey>))]
public readonly record struct DPoPProofKey : IStronglyTypedValue<DPoPProofKey>
{
/// <summary>
/// Determines whether this proof key value equals another proof key value.
/// </summary>
/// <param name="other">The other value to compare.</param>
/// <returns><see langword="true"/> when both values are equal; otherwise <see langword="false"/>.</returns>
public bool Equals(DPoPProofKey other) => Value == other.Value;

/// <summary>
/// Returns the hash code for the wrapped key string.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode() => Value.GetHashCode();

/// <summary>
Expand All @@ -25,6 +41,9 @@ namespace Duende.AccessTokenManagement.DPoP;

private readonly JsonWebKey _jsonWebKey;

/// <summary>
/// Returns the wrapped JWK string.
/// </summary>
public override string ToString() => Value;

private static readonly ValidationRule<string>[] Validators = [
Expand Down Expand Up @@ -53,6 +72,10 @@ private static ValidationRule<string> IsValidJsonWebKey() =>
/// You can't directly create this type.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
/// <summary>
Comment thread
damianh marked this conversation as resolved.
Outdated
/// Prevents creating an uninitialized <see cref="DPoPProofKey"/> instance.
/// </summary>
/// <exception cref="InvalidOperationException">Always thrown.</exception>
public DPoPProofKey() => throw new InvalidOperationException("Can't create null value");
private DPoPProofKey(string value)
{
Expand Down
Loading
Loading