Skip to content

Commit fbd6482

Browse files
committed
Allow overriding the maximum length when parsing refresh tokens
1 parent b15ff68 commit fbd6482

File tree

4 files changed

+254
-0
lines changed

4 files changed

+254
-0
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,24 @@ namespace Duende.AccessTokenManagement;
1111
public readonly record struct RefreshToken : IStronglyTypedValue<RefreshToken>
1212
{
1313
public const int MaxLength = 4 * 1024;
14+
1415
public override string ToString() => Value;
1516

17+
/// <summary>
18+
/// Changes the maximum length for refresh tokens.
19+
/// </summary>
20+
/// <param name="maxLength">The new maximum length. Must be a strictly positive value.</param>
21+
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="maxLength"/> is zero or a negative value.</exception>
22+
/// <remarks>
23+
/// Note that this change is applied statically and will affect all instances of <see cref="RefreshToken"/> across the entire application.
24+
/// </remarks>
25+
public static void SetMaxLength(int maxLength)
26+
{
27+
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxLength);
28+
29+
Validators[0] = ValidationRules.MaxLength(maxLength);
30+
}
31+
1632
private static readonly ValidationRule<string>[] Validators = [
1733
// Officially, there's no max length refresh tokens, but 4k is a good limit
1834
ValidationRules.MaxLength(MaxLength)

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
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
/// <summary>
7+
/// Tests for <see cref="RefreshToken.SetMaxLength"/>. These are isolated into a separate
8+
/// non-parallel collection because <see cref="RefreshToken.SetMaxLength"/> mutates static state
9+
/// that would affect other tests running concurrently.
10+
/// </summary>
11+
[Collection(nameof(RefreshTokenTests))]
12+
public class RefreshTokenSetMaxLengthTests : IDisposable
13+
{
14+
public void Dispose() =>
15+
// Reset to the default max length after each test so static state doesn't leak.
16+
RefreshToken.SetMaxLength(RefreshToken.MaxLength);
17+
18+
[Fact]
19+
public void SetMaxLength_AllowsLargerTokens()
20+
{
21+
// Arrange
22+
var largeMaxLength = 16 * 1024; // 16 KB, e.g. for ADFS tokens
23+
var largeValue = new string('a', RefreshToken.MaxLength + 1); // Exceeds default, within new limit
24+
RefreshToken.SetMaxLength(largeMaxLength);
25+
26+
// Act
27+
var result = RefreshToken.Parse(largeValue);
28+
29+
// Assert
30+
result.ToString().ShouldBe(largeValue);
31+
}
32+
33+
[Fact]
34+
public void SetMaxLength_StillRejectsTokensExceedingNewLimit()
35+
{
36+
// Arrange
37+
var newMaxLength = 8 * 1024;
38+
RefreshToken.SetMaxLength(newMaxLength);
39+
var tooLargeValue = new string('a', newMaxLength + 1);
40+
41+
// Act & Assert
42+
var exception = Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(tooLargeValue));
43+
exception.Message.ShouldContain("exceeds maximum length");
44+
}
45+
46+
[Fact]
47+
public void SetMaxLength_TryParse_AllowsLargerTokens()
48+
{
49+
// Arrange
50+
var largeMaxLength = 16 * 1024;
51+
var largeValue = new string('a', RefreshToken.MaxLength + 1);
52+
RefreshToken.SetMaxLength(largeMaxLength);
53+
54+
// Act
55+
var success = RefreshToken.TryParse(largeValue, out var result, out var errors);
56+
57+
// Assert
58+
success.ShouldBeTrue();
59+
result.ShouldNotBeNull();
60+
result.ToString().ShouldBe(largeValue);
61+
errors.ShouldBeEmpty();
62+
}
63+
64+
[Fact]
65+
public void SetMaxLength_TryParse_StillRejectsTokensExceedingNewLimit()
66+
{
67+
// Arrange
68+
var newMaxLength = 8 * 1024;
69+
RefreshToken.SetMaxLength(newMaxLength);
70+
var tooLargeValue = new string('a', newMaxLength + 1);
71+
72+
// Act
73+
var success = RefreshToken.TryParse(tooLargeValue, out var result, out var errors);
74+
75+
// Assert
76+
success.ShouldBeFalse();
77+
result.ShouldBeNull();
78+
errors.ShouldNotBeEmpty();
79+
errors[0].ShouldContain("exceeds maximum length");
80+
}
81+
82+
[Fact]
83+
public void SetMaxLength_CanReduceLimit()
84+
{
85+
// Arrange
86+
var smallerMaxLength = 100;
87+
RefreshToken.SetMaxLength(smallerMaxLength);
88+
var value = new string('a', smallerMaxLength + 1);
89+
90+
// Act & Assert
91+
var exception = Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(value));
92+
exception.Message.ShouldContain("exceeds maximum length");
93+
}
94+
95+
[Fact]
96+
public void SetMaxLength_AtExactNewLimit_Succeeds()
97+
{
98+
// Arrange
99+
var newMaxLength = 8 * 1024;
100+
RefreshToken.SetMaxLength(newMaxLength);
101+
var value = new string('a', newMaxLength);
102+
103+
// Act
104+
var result = RefreshToken.Parse(value);
105+
106+
// Assert
107+
result.ToString().ShouldBe(value);
108+
}
109+
110+
[Fact]
111+
public void SetMaxLength_Zero_ThrowsArgumentOutOfRangeException() =>
112+
// Act & Assert
113+
Should.Throw<ArgumentOutOfRangeException>(() => RefreshToken.SetMaxLength(0));
114+
115+
[Fact]
116+
public void SetMaxLength_Negative_ThrowsArgumentOutOfRangeException() =>
117+
// Act & Assert
118+
Should.Throw<ArgumentOutOfRangeException>(() => RefreshToken.SetMaxLength(-1));
119+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
/// <summary>
7+
/// Both the <see cref="RefreshTokenTests"/> and <see cref="RefreshTokenSetMaxLengthTests"/> classes share a collection to prevent parallel execution,
8+
/// since <see cref="RefreshToken.SetMaxLength"/> mutates static state.
9+
/// </summary>
10+
[Collection(nameof(RefreshTokenTests))]
11+
public class RefreshTokenTests
12+
{
13+
[Fact]
14+
public void Parse_ValidValue_ReturnsRefreshToken()
15+
{
16+
// Arrange
17+
var validValue = "valid_refresh_token";
18+
19+
// Act
20+
var result = RefreshToken.Parse(validValue);
21+
22+
// Assert
23+
result.ToString().ShouldBe(validValue);
24+
}
25+
26+
[Fact]
27+
public void Parse_InvalidValue_ThrowsException()
28+
{
29+
// Arrange
30+
var invalidValue = new string('a', RefreshToken.MaxLength + 1); // Exceeds max length
31+
32+
// Act & Assert
33+
var exception = Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(invalidValue));
34+
exception.Message.ShouldContain("exceeds maximum length");
35+
}
36+
37+
[Fact]
38+
public void TryParse_ValidValue_ReturnsTrueAndRefreshToken()
39+
{
40+
// Arrange
41+
var validValue = "valid_refresh_token";
42+
43+
// Act
44+
var success = RefreshToken.TryParse(validValue, out var result, out var errors);
45+
46+
// Assert
47+
success.ShouldBeTrue();
48+
result.ShouldNotBeNull();
49+
result.ToString().ShouldBe(validValue);
50+
errors.ShouldBeEmpty();
51+
}
52+
53+
[Fact]
54+
public void TryParse_InvalidValue_ReturnsFalseAndErrors()
55+
{
56+
// Arrange
57+
var invalidValue = new string('a', RefreshToken.MaxLength + 1); // Exceeds max length
58+
59+
// Act
60+
var success = RefreshToken.TryParse(invalidValue, out var result, out var errors);
61+
62+
// Assert
63+
success.ShouldBeFalse();
64+
result.ShouldBeNull();
65+
errors.ShouldNotBeEmpty();
66+
errors[0].ShouldContain("exceeds maximum length");
67+
}
68+
69+
[Fact]
70+
public void Parse_AtExactMaxLength_ReturnsRefreshToken()
71+
{
72+
// Arrange
73+
var value = new string('a', RefreshToken.MaxLength);
74+
75+
// Act
76+
var result = RefreshToken.Parse(value);
77+
78+
// Assert
79+
result.ToString().ShouldBe(value);
80+
}
81+
82+
[Fact]
83+
public void Parse_NullValue_ThrowsException() =>
84+
// Act & Assert
85+
Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(null!));
86+
87+
[Fact]
88+
public void Parse_EmptyValue_ThrowsException() =>
89+
// Act & Assert
90+
Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(string.Empty));
91+
92+
[Fact]
93+
public void Parse_WhitespaceValue_ThrowsException() =>
94+
// Act & Assert
95+
Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(" "));
96+
97+
[Fact]
98+
public void ParameterlessConstructor_ThrowsException()
99+
{
100+
// Act & Assert
101+
var exception = Should.Throw<InvalidOperationException>(() => new RefreshToken());
102+
exception.Message.ShouldContain("Can't create null value");
103+
}
104+
105+
[Fact]
106+
public void ImplicitStringConversion_ReturnsValue()
107+
{
108+
// Arrange
109+
var value = "my_refresh_token";
110+
var token = RefreshToken.Parse(value);
111+
112+
// Act
113+
string converted = token;
114+
115+
// Assert
116+
converted.ShouldBe(value);
117+
}
118+
}

0 commit comments

Comments
 (0)