diff --git a/Directory.Build.targets b/Directory.Build.targets
index a3106524..464a2d9a 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -63,6 +63,7 @@
$(DefineConstants);SUPPORTS_HTTP_CLIENT_DEFAULT_REQUEST_VERSION
+ $(DefineConstants);SUPPORTS_INTEGER32_RANDOM_NUMBER_GENERATOR_METHODS
+ /// Creates a new containing characters
+ /// randomly selected in the specified .
+ ///
+ /// The characters allowed to be included in the .
+ /// The desired length of the .
+ /// A new containing random data.
+ ///
+ /// The implementation resolved from is not valid.
+ ///
+ public static string CreateRandomString(ReadOnlySpan charset, int length)
+ {
+ var algorithm = CryptoConfig.CreateFromName("OpenIddict RNG Cryptographic Provider") switch
+ {
+ RandomNumberGenerator result => result,
+ null => null,
+ var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName))
+ };
+
+ try
+ {
+ var buffer = new char[length];
+
+ for (var index = 0; index < buffer.Length; index++)
+ {
+ // Pick a character in the specified charset by generating a random index.
+ buffer[index] = charset[index: algorithm switch
+ {
+#if SUPPORTS_INTEGER32_RANDOM_NUMBER_GENERATOR_METHODS
+ // If no custom random number generator was registered, use
+ // the static GetInt32() API on platforms that support it.
+ null => RandomNumberGenerator.GetInt32(0, charset.Length),
+#endif
+ // Otherwise, create a default implementation if necessary
+ // and use the local function that achieves the same result.
+ _ => GetInt32(algorithm ??= RandomNumberGenerator.Create(), 0..charset.Length)
+ }];
+ }
+
+ return new string(buffer);
+ }
+
+ finally
+ {
+ algorithm?.Dispose();
+ }
+
+ static int GetInt32(RandomNumberGenerator algorithm, Range range)
+ {
+ // Note: the logic used here is directly taken from the official implementation
+ // of the RandomNumberGenerator.GetInt32() method introduced in .NET Core 3.0.
+ //
+ // See https://github.com/dotnet/corefx/pull/31243 for more information.
+
+ var count = (uint) range.End.Value - (uint) range.Start.Value - 1;
+ if (count is 0)
+ {
+ return range.Start.Value;
+ }
+
+ var mask = count;
+ mask |= mask >> 1;
+ mask |= mask >> 2;
+ mask |= mask >> 4;
+ mask |= mask >> 8;
+ mask |= mask >> 16;
+
+ var buffer = new byte[sizeof(uint)];
+ uint value;
+
+ do
+ {
+ algorithm.GetBytes(buffer);
+
+ value = mask & BitConverter.ToUInt32(buffer, 0);
+ }
+
+ while (value > count);
+
+ return (int) value + range.Start.Value;
+ }
+ }
+
#if SUPPORTS_KEY_DERIVATION_WITH_SPECIFIED_HASH_ALGORITHM
///
/// Creates a derived key based on the specified using PBKDF2.
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
index 339f5845..c8bec667 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
@@ -1318,24 +1318,21 @@ public static partial class OpenIddictServerHandlers
descriptor.Payload = context.Token;
descriptor.Principal = context.Principal;
- // Note: unlike other reference tokens, user codes are meant to be used by humans,
- // who may have to enter it in a web form. To ensure it remains easy enough to type
- // even by users with non-Latin keyboards, user codes generated by OpenIddict are
- // only compound of 12 digits, generated using a crypto-secure random number generator.
- // In this case, the resulting user code is estimated to have at most ~40 bits of entropy.
if (context.TokenType is TokenTypeHints.UserCode)
{
do
{
- var array = OpenIddictHelpers.CreateRandomArray(size: 12);
- var builder = new StringBuilder(array.Length);
+ // Note: unlike other reference tokens, user codes are meant to be used by humans,
+ // who may have to enter it in a web form. To ensure it remains easy enough to type
+ // even by users with non-Latin keyboards, user codes generated by OpenIddict are
+ // only compound of 12 digits, generated using a crypto-secure random number generator.
+ // In this case, the resulting user code is estimated to have at most ~40 bits of entropy.
- for (var index = 0; index < array.Length; index += 4)
- {
- builder.AppendFormat(CultureInfo.InvariantCulture, "{0:D4}", BitConverter.ToUInt32(array, index) % 10000);
- }
+ static string CreateRandomNumericCode(int length) => OpenIddictHelpers.CreateRandomString(
+ charset: stackalloc[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' },
+ length: length);
- descriptor.ReferenceId = builder.ToString();
+ descriptor.ReferenceId = CreateRandomNumericCode(length: 12);
}
// User codes are relatively short. To help reduce the risks of collisions with
@@ -1343,9 +1340,9 @@ public static partial class OpenIddictServerHandlers
while (await _tokenManager.FindByReferenceIdAsync(descriptor.ReferenceId) is not null);
}
- // For other tokens, generate a base64url-encoded 256-bit random identifier.
else
{
+ // For other tokens, generate a base64url-encoded 256-bit random identifier.
descriptor.ReferenceId = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 256));
}