Skip to content

Commit 7c33562

Browse files
committed
Delegate Base64 encoding to a more performant implementation
1 parent 5f15bce commit 7c33562

File tree

2 files changed

+134
-16
lines changed

2 files changed

+134
-16
lines changed

src/ModelContextProtocol/Auth/OAuthService.cs

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text;
55
using System.Text.Json;
66
using ModelContextProtocol.Auth.Types;
7+
using ModelContextProtocol.Utils;
78
using ModelContextProtocol.Utils.Json;
89

910
namespace ModelContextProtocol.Auth;
@@ -54,14 +55,8 @@ public static string GenerateCodeVerifier()
5455
using var rng = RandomNumberGenerator.Create();
5556
rng.GetBytes(bytes);
5657

57-
// Base64url encode the random bytes
58-
var base64 = Convert.ToBase64String(bytes);
59-
var base64Url = base64
60-
.Replace('+', '-')
61-
.Replace('/', '_')
62-
.Replace("=", "");
63-
64-
return base64Url;
58+
// Use the optimized Base64UrlHelpers for encoding
59+
return Base64UrlHelpers.Encode(bytes);
6560
}
6661

6762
/// <summary>
@@ -75,14 +70,8 @@ public static string GenerateCodeChallenge(string codeVerifier)
7570
using var sha256 = SHA256.Create();
7671
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
7772

78-
// Base64url encode the hash
79-
var base64 = Convert.ToBase64String(challengeBytes);
80-
var base64Url = base64
81-
.Replace('+', '-')
82-
.Replace('/', '_')
83-
.Replace("=", "");
84-
85-
return base64Url;
73+
// Use the optimized Base64UrlHelpers for encoding
74+
return Base64UrlHelpers.Encode(challengeBytes);
8675
}
8776

8877
/// <summary>
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using System.Text;
2+
3+
namespace ModelContextProtocol.Utils
4+
{
5+
/// <summary>
6+
/// Helper methods for Base64Url encoding and decoding.
7+
/// Based on implementation from MSAL (https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/main/src/client/Microsoft.Identity.Client/Utils/Base64UrlHelpers.cs)
8+
/// </summary>
9+
internal static class Base64UrlHelpers
10+
{
11+
private const char base64UrlCharacter62 = '-';
12+
private const char base64UrlCharacter63 = '_';
13+
14+
/// <summary>
15+
/// Encoding table for base64url encoding
16+
/// </summary>
17+
internal static readonly char[] s_base64Table =
18+
{
19+
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
20+
'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
21+
'0','1','2','3','4','5','6','7','8','9',
22+
base64UrlCharacter62,
23+
base64UrlCharacter63
24+
};
25+
26+
/// <summary>
27+
/// Converts an array of bytes to a base64url encoded string.
28+
/// </summary>
29+
/// <param name="inArray">An array of 8-bit unsigned integers.</param>
30+
/// <returns>The string representation in base64url encoding of inArray.</returns>
31+
public static string Encode(byte[] inArray)
32+
{
33+
if (inArray == null)
34+
return string.Empty;
35+
36+
return Encode(inArray, 0, inArray.Length);
37+
}
38+
39+
/// <summary>
40+
/// Converts a subset of an array of 8-bit unsigned integers to its equivalent string representation that is encoded with base-64-url digits.
41+
/// </summary>
42+
/// <param name="inArray">An array of 8-bit unsigned integers.</param>
43+
/// <param name="offset">An offset in inArray.</param>
44+
/// <param name="length">The number of elements of inArray to convert.</param>
45+
/// <returns>The string representation in base64url encoding of length elements of inArray, starting at position offset.</returns>
46+
private static string Encode(byte[] inArray, int offset, int length)
47+
{
48+
_ = inArray ?? throw new ArgumentNullException(nameof(inArray));
49+
50+
if (length == 0)
51+
return string.Empty;
52+
53+
if (length < 0)
54+
throw new ArgumentOutOfRangeException(nameof(length));
55+
56+
if (offset < 0 || inArray.Length < offset)
57+
throw new ArgumentOutOfRangeException(nameof(offset));
58+
59+
if (inArray.Length < offset + length)
60+
throw new ArgumentOutOfRangeException(nameof(length));
61+
62+
int lengthmod3 = length % 3;
63+
int limit = offset + (length - lengthmod3);
64+
char[] output = new char[(length + 2) / 3 * 4];
65+
char[] table = s_base64Table;
66+
int i, j = 0;
67+
68+
// Process three bytes at a time, each three bytes becomes four base64 characters
69+
for (i = offset; i < limit; i += 3)
70+
{
71+
byte d0 = inArray[i];
72+
byte d1 = inArray[i + 1];
73+
byte d2 = inArray[i + 2];
74+
75+
output[j + 0] = table[d0 >> 2];
76+
output[j + 1] = table[((d0 & 0x03) << 4) | (d1 >> 4)];
77+
output[j + 2] = table[((d1 & 0x0f) << 2) | (d2 >> 6)];
78+
output[j + 3] = table[d2 & 0x3f];
79+
j += 4;
80+
}
81+
82+
// Handle remaining bytes and padding
83+
i = limit;
84+
85+
switch (lengthmod3)
86+
{
87+
case 2:
88+
{
89+
byte d0 = inArray[i];
90+
byte d1 = inArray[i + 1];
91+
92+
output[j + 0] = table[d0 >> 2];
93+
output[j + 1] = table[((d0 & 0x03) << 4) | (d1 >> 4)];
94+
output[j + 2] = table[(d1 & 0x0f) << 2];
95+
j += 3;
96+
}
97+
break;
98+
99+
case 1:
100+
{
101+
byte d0 = inArray[i];
102+
103+
output[j + 0] = table[d0 >> 2];
104+
output[j + 1] = table[(d0 & 0x03) << 4];
105+
j += 2;
106+
}
107+
break;
108+
109+
// Default or case 0: no further operations are needed.
110+
}
111+
112+
// Return the result without creating any additional string allocations
113+
return new string(output, 0, j);
114+
}
115+
116+
/// <summary>
117+
/// Encodes a string using base64url encoding.
118+
/// </summary>
119+
/// <param name="str">The string to encode.</param>
120+
/// <returns>Base64Url encoding of the UTF8 bytes of the input string.</returns>
121+
public static string EncodeString(string str)
122+
{
123+
if (str == null)
124+
throw new ArgumentNullException(nameof(str), "Input string cannot be null.");
125+
126+
return Encode(Encoding.UTF8.GetBytes(str)) ?? string.Empty;
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)