Skip to content

Commit 8883594

Browse files
authored
Merge pull request #350 from DuendeSoftware/wca/override-refresh-token-max-length
Allow overriding the maximum length when parsing refresh tokens
2 parents 635f140 + cd961c8 commit 8883594

File tree

6 files changed

+271
-5
lines changed

6 files changed

+271
-5
lines changed

access-token-management/src/AccessTokenManagement/RefreshToken.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,36 @@ namespace Duende.AccessTokenManagement;
1010
[JsonConverter(typeof(StringValueJsonConverter<RefreshToken>))]
1111
public readonly record struct RefreshToken : IStronglyTypedValue<RefreshToken>
1212
{
13+
private static int? _overriddenMaxLength;
14+
private static ValidationRule<string>[] _validators = [];
15+
16+
// Officially, there's no max length refresh tokens, but 4k is a good limit
1317
public const int MaxLength = 4 * 1024;
18+
19+
static RefreshToken() => InitializeValidators();
20+
1421
public override string ToString() => Value;
1522

16-
private static readonly ValidationRule<string>[] Validators = [
17-
// Officially, there's no max length refresh tokens, but 4k is a good limit
18-
ValidationRules.MaxLength(MaxLength)
19-
];
23+
/// <summary>
24+
/// Changes the maximum length for refresh tokens.
25+
/// </summary>
26+
/// <param name="maxLength">The new maximum length. Must be a strictly positive value.</param>
27+
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="maxLength"/> is zero or a negative value.</exception>
28+
/// <remarks>
29+
/// Note that this change is applied statically and will affect all instances of <see cref="RefreshToken"/> across the entire application.
30+
/// </remarks>
31+
public static void SetMaxLength(int maxLength)
32+
{
33+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxLength);
34+
35+
_overriddenMaxLength = maxLength;
36+
InitializeValidators();
37+
}
38+
39+
private static void InitializeValidators() =>
40+
_validators = [
41+
ValidationRules.MaxLength(_overriddenMaxLength ?? MaxLength)
42+
];
2043

2144
/// <summary>
2245
/// You can't directly create this type.
@@ -38,7 +61,7 @@ namespace Duende.AccessTokenManagement;
3861
/// and also includes a list of errors. This is useful for validating user input or other scenarios where you want to provide feedback
3962
/// </summary>
4063
public static bool TryParse(string value, [NotNullWhen(true)] out RefreshToken? parsed, out string[] errors) =>
41-
IStronglyTypedValue<RefreshToken>.TryBuildValidatedObject(value, Validators, out parsed, out errors);
64+
IStronglyTypedValue<RefreshToken>.TryBuildValidatedObject(value, _validators, out parsed, out errors);
4265

4366
static RefreshToken IStronglyTypedValue<RefreshToken>.Create(string result) => new(result);
4467

access-token-management/test/AccessTokenManagement.Tests/AccessTokenHandler/AccessTokenHandlerTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
namespace Duende.AccessTokenManagement.AccessTokenHandler;
1313

14+
// This class uses <see cref="RefreshToken"/>, whose max length is static mutable state
15+
// (via <see cref="RefreshToken.SetMaxLength"/>). Sharing a collection with the other
16+
// RefreshToken test classes prevents parallel execution that could cause flaky failures.
17+
[Collection(nameof(Types.RefreshTokenTests))]
1418
public class AccessTokenHandlerTests(ITestOutputHelper output)
1519
{
1620
private readonly CancellationToken _ct = TestContext.Current.CancellationToken;

access-token-management/test/AccessTokenManagement.Tests/PublicApiVerificationTests.VerifyPublicApi.verified.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
public RefreshToken() { }
185185
public override string ToString() { }
186186
public static Duende.AccessTokenManagement.RefreshToken Parse(string value) { }
187+
public static void SetMaxLength(int maxLength) { }
187188
public static bool TryParse(string value, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Duende.AccessTokenManagement.RefreshToken? parsed, out string[] errors) { }
188189
public static string op_Implicit(Duende.AccessTokenManagement.RefreshToken value) { }
189190
}

access-token-management/test/AccessTokenManagement.Tests/StoreTokensInAuthenticationPropertiesTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111
namespace Duende.AccessTokenManagement;
1212

13+
// This class uses <see cref="RefreshToken"/>, whose max length is static mutable state
14+
// (via <see cref="RefreshToken.SetMaxLength"/>). Sharing a collection with the other
15+
// RefreshToken test classes prevents parallel execution that could cause flaky failures.
16+
[Collection(nameof(Types.RefreshTokenTests))]
1317
public class StoreTokensInAuthenticationPropertiesTests
1418
{
1519
private readonly CancellationToken _ct = TestContext.Current.CancellationToken;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
namespace Duende.AccessTokenManagement.Types;
5+
6+
// All test classes that use <see cref="RefreshToken"/> share this collection because
7+
// <see cref="RefreshToken.SetMaxLength"/> mutates static state. Serializing them
8+
// prevents flaky failures from concurrent reads/writes of the max length.
9+
[Collection(nameof(RefreshTokenTests))]
10+
public class RefreshTokenSetMaxLengthTests : IDisposable
11+
{
12+
public void Dispose() =>
13+
// Reset to the default max length after each test so static state doesn't leak.
14+
RefreshToken.SetMaxLength(RefreshToken.MaxLength);
15+
16+
[Fact]
17+
public void SetMaxLength_AllowsLargerTokens()
18+
{
19+
// Arrange
20+
var largeMaxLength = 16 * 1024; // 16 KB, e.g. for ADFS tokens
21+
var largeValue = new string('a', RefreshToken.MaxLength + 1); // Exceeds default, within new limit
22+
RefreshToken.SetMaxLength(largeMaxLength);
23+
24+
// Act
25+
var result = RefreshToken.Parse(largeValue);
26+
27+
// Assert
28+
result.ToString().ShouldBe(largeValue);
29+
}
30+
31+
[Fact]
32+
public void SetMaxLength_StillRejectsTokensExceedingNewLimit()
33+
{
34+
// Arrange
35+
var newMaxLength = 8 * 1024;
36+
RefreshToken.SetMaxLength(newMaxLength);
37+
var tooLargeValue = new string('a', newMaxLength + 1);
38+
39+
// Act & Assert
40+
var exception = Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(tooLargeValue));
41+
exception.Message.ShouldContain("exceeds maximum length");
42+
}
43+
44+
[Fact]
45+
public void SetMaxLength_TryParse_AllowsLargerTokens()
46+
{
47+
// Arrange
48+
var largeMaxLength = 16 * 1024;
49+
var largeValue = new string('a', RefreshToken.MaxLength + 1);
50+
RefreshToken.SetMaxLength(largeMaxLength);
51+
52+
// Act
53+
var success = RefreshToken.TryParse(largeValue, out var result, out var errors);
54+
55+
// Assert
56+
success.ShouldBeTrue();
57+
result.ShouldNotBeNull();
58+
result.ToString().ShouldBe(largeValue);
59+
errors.ShouldBeEmpty();
60+
}
61+
62+
[Fact]
63+
public void SetMaxLength_TryParse_StillRejectsTokensExceedingNewLimit()
64+
{
65+
// Arrange
66+
var newMaxLength = 8 * 1024;
67+
RefreshToken.SetMaxLength(newMaxLength);
68+
var tooLargeValue = new string('a', newMaxLength + 1);
69+
70+
// Act
71+
var success = RefreshToken.TryParse(tooLargeValue, out var result, out var errors);
72+
73+
// Assert
74+
success.ShouldBeFalse();
75+
result.ShouldBeNull();
76+
errors.ShouldNotBeEmpty();
77+
errors[0].ShouldContain("exceeds maximum length");
78+
}
79+
80+
[Fact]
81+
public void SetMaxLength_CanReduceLimit()
82+
{
83+
// Arrange
84+
var smallerMaxLength = 100;
85+
RefreshToken.SetMaxLength(smallerMaxLength);
86+
var value = new string('a', smallerMaxLength + 1);
87+
88+
// Act & Assert
89+
var exception = Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(value));
90+
exception.Message.ShouldContain("exceeds maximum length");
91+
}
92+
93+
[Fact]
94+
public void SetMaxLength_AtExactNewLimit_Succeeds()
95+
{
96+
// Arrange
97+
var newMaxLength = 8 * 1024;
98+
RefreshToken.SetMaxLength(newMaxLength);
99+
var value = new string('a', newMaxLength);
100+
101+
// Act
102+
var result = RefreshToken.Parse(value);
103+
104+
// Assert
105+
result.ToString().ShouldBe(value);
106+
}
107+
108+
[Fact]
109+
public void SetMaxLength_Zero_ThrowsArgumentOutOfRangeException() =>
110+
// Act & Assert
111+
Should.Throw<ArgumentOutOfRangeException>(() => RefreshToken.SetMaxLength(0));
112+
113+
[Fact]
114+
public void SetMaxLength_Negative_ThrowsArgumentOutOfRangeException() =>
115+
// Act & Assert
116+
Should.Throw<ArgumentOutOfRangeException>(() => RefreshToken.SetMaxLength(-1));
117+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
namespace Duende.AccessTokenManagement.Types;
5+
6+
// All test classes that use <see cref="RefreshToken"/> share this collection because
7+
// <see cref="RefreshToken.SetMaxLength"/> mutates static state. Serializing them
8+
// prevents flaky failures from concurrent reads/writes of the max length.
9+
[Collection(nameof(RefreshTokenTests))]
10+
public class RefreshTokenTests
11+
{
12+
[Fact]
13+
public void Parse_ValidValue_ReturnsRefreshToken()
14+
{
15+
// Arrange
16+
var validValue = "valid_refresh_token";
17+
18+
// Act
19+
var result = RefreshToken.Parse(validValue);
20+
21+
// Assert
22+
result.ToString().ShouldBe(validValue);
23+
}
24+
25+
[Fact]
26+
public void Parse_InvalidValue_ThrowsException()
27+
{
28+
// Arrange
29+
var invalidValue = new string('a', RefreshToken.MaxLength + 1); // Exceeds max length
30+
31+
// Act & Assert
32+
var exception = Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(invalidValue));
33+
exception.Message.ShouldContain("exceeds maximum length");
34+
}
35+
36+
[Fact]
37+
public void TryParse_ValidValue_ReturnsTrueAndRefreshToken()
38+
{
39+
// Arrange
40+
var validValue = "valid_refresh_token";
41+
42+
// Act
43+
var success = RefreshToken.TryParse(validValue, out var result, out var errors);
44+
45+
// Assert
46+
success.ShouldBeTrue();
47+
result.ShouldNotBeNull();
48+
result.ToString().ShouldBe(validValue);
49+
errors.ShouldBeEmpty();
50+
}
51+
52+
[Fact]
53+
public void TryParse_InvalidValue_ReturnsFalseAndErrors()
54+
{
55+
// Arrange
56+
var invalidValue = new string('a', RefreshToken.MaxLength + 1); // Exceeds max length
57+
58+
// Act
59+
var success = RefreshToken.TryParse(invalidValue, out var result, out var errors);
60+
61+
// Assert
62+
success.ShouldBeFalse();
63+
result.ShouldBeNull();
64+
errors.ShouldNotBeEmpty();
65+
errors[0].ShouldContain("exceeds maximum length");
66+
}
67+
68+
[Fact]
69+
public void Parse_AtExactMaxLength_ReturnsRefreshToken()
70+
{
71+
// Arrange
72+
var value = new string('a', RefreshToken.MaxLength);
73+
74+
// Act
75+
var result = RefreshToken.Parse(value);
76+
77+
// Assert
78+
result.ToString().ShouldBe(value);
79+
}
80+
81+
[Fact]
82+
public void Parse_NullValue_ThrowsException() =>
83+
// Act & Assert
84+
Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(null!));
85+
86+
[Fact]
87+
public void Parse_EmptyValue_ThrowsException() =>
88+
// Act & Assert
89+
Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(string.Empty));
90+
91+
[Fact]
92+
public void Parse_WhitespaceValue_ThrowsException() =>
93+
// Act & Assert
94+
Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(" "));
95+
96+
[Fact]
97+
public void ParameterlessConstructor_ThrowsException()
98+
{
99+
// Act & Assert
100+
var exception = Should.Throw<InvalidOperationException>(() => new RefreshToken());
101+
exception.Message.ShouldContain("Can't create null value");
102+
}
103+
104+
[Fact]
105+
public void ImplicitStringConversion_ReturnsValue()
106+
{
107+
// Arrange
108+
var value = "my_refresh_token";
109+
var token = RefreshToken.Parse(value);
110+
111+
// Act
112+
string converted = token;
113+
114+
// Assert
115+
converted.ShouldBe(value);
116+
}
117+
}

0 commit comments

Comments
 (0)