Skip to content

Commit 671bb11

Browse files
Merge pull request #63 from topcoder-04/main
M2M token support
2 parents b027482 + 080932b commit 671bb11

File tree

9 files changed

+220
-13
lines changed

9 files changed

+220
-13
lines changed

src/Clerk/BackendAPI/Helpers/AuthenticateRequest.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public static async Task<RequestState> AuthenticateRequestAsync(
4040
{
4141
TokenType.SessionToken => "session_token",
4242
TokenType.MachineToken => "machine_token",
43+
TokenType.MachineTokenV2 => "m2m_token",
4344
TokenType.OAuthToken => "oauth_token",
4445
TokenType.ApiKey => "api_key",
4546
_ => tokenType.ToString().ToLowerInvariant()
@@ -48,18 +49,31 @@ public static async Task<RequestState> AuthenticateRequestAsync(
4849
// Check if token type is accepted
4950
if (!options.AcceptsToken.Contains("any") && !options.AcceptsToken.Contains(tokenTypeName))
5051
{
51-
return RequestState.SignedOut(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED);
52+
// Special case: if acceptsToken contains "machine_token", accept both MachineToken and MachineTokenV2
53+
bool isAccepted = false;
54+
if (options.AcceptsToken.Contains("machine_token") &&
55+
(tokenType == TokenType.MachineToken || tokenType == TokenType.MachineTokenV2))
56+
{
57+
isAccepted = true;
58+
}
59+
60+
if (!isAccepted)
61+
{
62+
return RequestState.SignedOut(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED);
63+
}
5264
}
5365

5466
VerifyTokenOptions verifyTokenOptions;
5567

5668
if (TokenTypeHelper.IsMachineToken(sessionToken))
5769
{
58-
// Machine tokens require secret key for API verification
59-
if (options.SecretKey == null)
70+
if (options.SecretKey == null && options.MachineSecretKey == null)
6071
return RequestState.SignedOut(AuthErrorReason.SECRET_KEY_MISSING);
6172

62-
verifyTokenOptions = new VerifyTokenOptions(secretKey: options.SecretKey);
73+
verifyTokenOptions = new VerifyTokenOptions(
74+
secretKey: options.SecretKey,
75+
machineSecretKey: options.MachineSecretKey
76+
);
6377
}
6478
else
6579
{

src/Clerk/BackendAPI/Helpers/AuthenticateRequestOptions.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,36 @@ public sealed class AuthenticateRequestOptions
1111
public readonly long ClockSkewInMs;
1212
public readonly string? JwtKey;
1313
public readonly string? SecretKey;
14+
public readonly string? MachineSecretKey;
1415
public readonly IEnumerable<string> AcceptsToken;
1516

1617
/// <summary>
1718
/// Options to configure AuthenticateRequestAsync.
1819
/// </summary>
1920
/// <param name="secretKey">The Clerk secret key from the API Keys page in the Clerk Dashboard. (Optional)</param>
21+
/// <param name="machineSecretKey">The Machine secret key for machine-specific authentication. (Optional)</param>
2022
/// <param name="jwtKey">PEM Public String used to verify the session token in a networkless manner. (Optional)</param>
2123
/// <param name="audiences">A list of audiences to verify against.</param>
2224
/// <param name="authorizedParties">An allowlist of origins to verify against.</param>
2325
/// <param name="clockSkewInMs">
2426
/// Allowed time difference (in milliseconds) between the Clerk server (which generates the
25-
/// token) and the clock of the user's application server when validating a token. Defaults to 5000 ms.
27+
/// token) and the user's application server when validating a token. Defaults to 5000 ms.
2628
/// </param>
2729
/// <param name="acceptsToken">A list of token types to accept. Defaults to ["any"].</param>
2830
public AuthenticateRequestOptions(
2931
string? secretKey = null,
32+
string? machineSecretKey = null,
3033
string? jwtKey = null,
3134
IEnumerable<string>? audiences = null,
3235
IEnumerable<string>? authorizedParties = null,
3336
long? clockSkewInMs = null,
3437
IEnumerable<string>? acceptsToken = null)
3538
{
36-
if (string.IsNullOrEmpty(secretKey) && string.IsNullOrEmpty(jwtKey))
39+
if (string.IsNullOrEmpty(secretKey) && string.IsNullOrEmpty(jwtKey) && string.IsNullOrEmpty(machineSecretKey))
3740
throw new AuthenticateRequestException(AuthErrorReason.SECRET_KEY_MISSING);
3841

3942
SecretKey = secretKey;
43+
MachineSecretKey = machineSecretKey;
4044
JwtKey = jwtKey;
4145
Audiences = audiences;
4246
AuthorizedParties = authorizedParties ?? new List<string>();

src/Clerk/BackendAPI/Helpers/RequestState.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ public AuthObject ToAuth()
128128
Claims = Claims.Claims.GroupBy(c => c.Type).ToDictionary(g => g.Key, g => (object)g.Select(c => c.Value).ToList())
129129
};
130130
case TokenType.MachineToken:
131+
case TokenType.MachineTokenV2:
131132
return new M2MMachineAuthObject
132133
{
133134
Id = Claims.FindFirst("id")?.Value,

src/Clerk/BackendAPI/Helpers/TokenTypes.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public enum TokenType
99
{
1010
SessionToken,
1111
MachineToken,
12+
MachineTokenV2,
1213
OAuthToken,
1314
ApiKey
1415
}
@@ -19,6 +20,7 @@ public enum TokenType
1920
public static class TokenPrefix
2021
{
2122
public const string MachineToken = "mt_";
23+
public const string MachineTokenV2 = "m2m_";
2224
public const string OAuthToken = "oat_";
2325
public const string ApiKey = "ak_";
2426
}
@@ -28,7 +30,7 @@ public static class TokenPrefix
2830
/// </summary>
2931
public static class TokenTypeHelper
3032
{
31-
private static readonly string[] MachineTokenPrefixes = { TokenPrefix.MachineToken, TokenPrefix.OAuthToken, TokenPrefix.ApiKey };
33+
private static readonly string[] MachineTokenPrefixes = { TokenPrefix.MachineToken, TokenPrefix.MachineTokenV2, TokenPrefix.OAuthToken, TokenPrefix.ApiKey };
3234

3335
/// <summary>
3436
/// Determines if a token is a machine token (includes M2M, OAuth, and API key tokens)
@@ -62,6 +64,9 @@ public static TokenType GetTokenType(string token)
6264
if (token.StartsWith(TokenPrefix.MachineToken))
6365
return TokenType.MachineToken;
6466

67+
if (token.StartsWith(TokenPrefix.MachineTokenV2))
68+
return TokenType.MachineTokenV2;
69+
6570
if (token.StartsWith(TokenPrefix.ApiKey))
6671
return TokenType.ApiKey;
6772

@@ -81,6 +86,7 @@ public static string GetVerificationEndpoint(TokenType tokenType)
8186
return tokenType switch
8287
{
8388
TokenType.MachineToken => "/m2m_tokens/verify",
89+
TokenType.MachineTokenV2 => "/m2m_tokens/verify",
8490
TokenType.OAuthToken => "/oauth_applications/access_tokens/verify",
8591
TokenType.ApiKey => "/api_keys/verify",
8692
_ => throw new ArgumentException($"No verification endpoint for token type: {tokenType}")

src/Clerk/BackendAPI/Helpers/VerifyToken.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,14 +255,16 @@ private static async Task<WellKnownJWKS> FetchJwksAsync(VerifyTokenOptions optio
255255
/// <returns>ClaimsPrincipal containing token information</returns>
256256
private static async Task<ClaimsPrincipal> VerifyMachineTokenAsync(string token, VerifyTokenOptions options, TokenType tokenType)
257257
{
258-
if (options.SecretKey == null)
258+
if (options.SecretKey == null && options.MachineSecretKey == null)
259259
throw new TokenVerificationException(TokenVerificationErrorReason.SECRET_KEY_MISSING);
260260

261261
var endpoint = TokenTypeHelper.GetVerificationEndpoint(tokenType);
262262
var verificationUrl = $"{options.ApiUrl}/{options.ApiVersion}{endpoint}";
263263

264264
using var client = new HttpClient();
265-
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.SecretKey);
265+
266+
var authToken = options.SecretKey ?? options.MachineSecretKey;
267+
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken);
266268
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
267269

268270
var payload = new { secret = token };

src/Clerk/BackendAPI/Helpers/VerifyTokenOptions.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ public sealed class VerifyTokenOptions
1515
public readonly string? JwtKey;
1616

1717
public readonly string? SecretKey;
18+
public readonly string? MachineSecretKey;
1819

1920
/// <summary>
2021
/// Options to configure VerifyTokenAsync.
2122
/// </summary>
2223
/// <param name="secretKey">The Clerk secret key from the API Keys page in the Clerk Dashboard. (Optional)</param>
24+
/// <param name="machineSecretKey">The Machine secret key for machine-specific authentication. (Optional)</param>
2325
/// <param name="jwtKey">PEM Public String used to verify the session token in a networkless manner. (Optional)</param>
2426
/// <param name="audiences">A list of audiences to verify against.</param>
2527
/// <param name="authorizedParties">An allowlist of origins to verify against.</param>
@@ -31,17 +33,19 @@ public sealed class VerifyTokenOptions
3133
/// <param name="apiVersion">The version passed to the Clerk API. Defaults to 'v1'</param>
3234
public VerifyTokenOptions(
3335
string? secretKey = null,
36+
string? machineSecretKey = null,
3437
string? jwtKey = null,
3538
IEnumerable<string>? audiences = null,
3639
IEnumerable<string>? authorizedParties = null,
3740
long? clockSkewInMs = null,
3841
string? apiUrl = null,
3942
string? apiVersion = null)
4043
{
41-
if (string.IsNullOrEmpty(secretKey) && string.IsNullOrEmpty(jwtKey))
44+
if (string.IsNullOrEmpty(secretKey) && string.IsNullOrEmpty(jwtKey) && string.IsNullOrEmpty(machineSecretKey))
4245
throw new TokenVerificationException(TokenVerificationErrorReason.SECRET_KEY_MISSING);
4346

4447
SecretKey = secretKey;
48+
MachineSecretKey = machineSecretKey;
4549
JwtKey = jwtKey;
4650
Audiences = audiences;
4751
AuthorizedParties = authorizedParties;

tests/Hooks/BeforeRequestHookTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public async Task BeforeRequestAsync_AddsClerkApiVersionHeader()
1818
var result = await hook.BeforeRequestAsync(hookCtx, request);
1919

2020
Assert.True(result.Headers.Contains("Clerk-API-Version"));
21-
Assert.Equal("2024-10-01", result.Headers.GetValues("Clerk-API-Version").First());
21+
Assert.Equal("2025-04-10", result.Headers.GetValues("Clerk-API-Version").First());
2222
}
2323

2424
[Fact]

tests/JwksHelpers/AuthenticateRequestTests.cs

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ public async Task TestRealOAuthTokenClaims()
424424

425425
[Theory]
426426
[InlineData("mt_1234567890abcdef", TokenType.MachineToken)]
427+
[InlineData("m2m_1234567890abcdef", TokenType.MachineTokenV2)]
427428
[InlineData("oat_1234567890abcdef", TokenType.OAuthToken)]
428429
[InlineData("ak_1234567890abcdef", TokenType.ApiKey)]
429430
[InlineData("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9...", TokenType.SessionToken)]
@@ -594,13 +595,58 @@ public async Task TestMachineTokenWithSecretKey()
594595
var httpContext = CreateHttpContextWithToken("mt_test_token");
595596
var state = await AuthenticateRequest.AuthenticateRequestAsync(httpContext.Request, arOptions);
596597

597-
// Should attempt verification (will fail due to no real HTTP client, but won't fail on secret key)
598598
Assert.NotEqual(AuthErrorReason.SECRET_KEY_MISSING, state.ErrorReason);
599599
Assert.NotEqual(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
600600
}
601601

602+
[Fact]
603+
public async Task TestMachineTokenWithMachineSecretKey()
604+
{
605+
var arOptions = new AuthenticateRequestOptions(
606+
machineSecretKey: "ms_test_machine_secret",
607+
acceptsToken: new[] { "any" }
608+
);
609+
610+
var httpContext = CreateHttpContextWithToken("mt_test_token");
611+
var state = await AuthenticateRequest.AuthenticateRequestAsync(httpContext.Request, arOptions);
612+
613+
Assert.NotEqual(AuthErrorReason.SECRET_KEY_MISSING, state.ErrorReason);
614+
Assert.NotEqual(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
615+
}
616+
617+
[Fact]
618+
public async Task TestMachineTokenWithBothKeys()
619+
{
620+
var arOptions = new AuthenticateRequestOptions(
621+
secretKey: "sk_test_secret",
622+
machineSecretKey: "ms_test_machine_secret",
623+
acceptsToken: new[] { "any" }
624+
);
625+
626+
var httpContext = CreateHttpContextWithToken("mt_test_token");
627+
var state = await AuthenticateRequest.AuthenticateRequestAsync(httpContext.Request, arOptions);
628+
629+
Assert.NotEqual(AuthErrorReason.SECRET_KEY_MISSING, state.ErrorReason);
630+
Assert.NotEqual(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
631+
}
632+
633+
[Fact]
634+
public async Task TestMachineTokenWithNoKeys()
635+
{
636+
var arOptions = new AuthenticateRequestOptions(
637+
jwtKey: "test-jwt-key",
638+
acceptsToken: new[] { "any" }
639+
);
640+
641+
var httpContext = CreateHttpContextWithToken("mt_test_token");
642+
var state = await AuthenticateRequest.AuthenticateRequestAsync(httpContext.Request, arOptions);
643+
644+
Assert.Equal(AuthErrorReason.SECRET_KEY_MISSING, state.ErrorReason);
645+
}
646+
602647
[Theory]
603648
[InlineData("mt_machine_token_123")]
649+
[InlineData("m2m_machine_token_123")]
604650
[InlineData("oat_oauth_token_123")]
605651
[InlineData("ak_api_key_123")]
606652
public async Task TestDifferentMachineTokenPrefixes(string token)
@@ -618,6 +664,109 @@ public async Task TestDifferentMachineTokenPrefixes(string token)
618664
Assert.NotEqual(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
619665
}
620666

667+
[Fact]
668+
public async Task TestM2MTokenWithSecretKey()
669+
{
670+
var arOptions = new AuthenticateRequestOptions(
671+
secretKey: "sk_test_secret",
672+
acceptsToken: new[] { "m2m_token" }
673+
);
674+
675+
var httpContext = CreateHttpContextWithToken("m2m_test_token");
676+
var state = await AuthenticateRequest.AuthenticateRequestAsync(httpContext.Request, arOptions);
677+
678+
Assert.NotEqual(AuthErrorReason.SECRET_KEY_MISSING, state.ErrorReason);
679+
Assert.NotEqual(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
680+
}
681+
682+
[Fact]
683+
public async Task TestM2MTokenWithMachineSecretKey()
684+
{
685+
var arOptions = new AuthenticateRequestOptions(
686+
machineSecretKey: "ms_test_machine_secret",
687+
acceptsToken: new[] { "m2m_token" }
688+
);
689+
690+
var httpContext = CreateHttpContextWithToken("m2m_test_token");
691+
var state = await AuthenticateRequest.AuthenticateRequestAsync(httpContext.Request, arOptions);
692+
693+
Assert.NotEqual(AuthErrorReason.SECRET_KEY_MISSING, state.ErrorReason);
694+
Assert.NotEqual(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
695+
}
696+
697+
[Fact]
698+
public async Task TestM2MTokenWithBothKeys()
699+
{
700+
var arOptions = new AuthenticateRequestOptions(
701+
secretKey: "sk_test_secret",
702+
machineSecretKey: "ms_test_machine_secret",
703+
acceptsToken: new[] { "m2m_token" }
704+
);
705+
706+
var httpContext = CreateHttpContextWithToken("m2m_test_token");
707+
var state = await AuthenticateRequest.AuthenticateRequestAsync(httpContext.Request, arOptions);
708+
709+
Assert.NotEqual(AuthErrorReason.SECRET_KEY_MISSING, state.ErrorReason);
710+
Assert.NotEqual(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
711+
}
712+
713+
[Fact]
714+
public async Task TestM2MTokenTypeAcceptance()
715+
{
716+
var arOptions = new AuthenticateRequestOptions(
717+
secretKey: "sk_test_secret",
718+
acceptsToken: new[] { "m2m_token" }
719+
);
720+
721+
var httpContext = CreateHttpContextWithToken("m2m_test_token");
722+
var state = await AuthenticateRequest.AuthenticateRequestAsync(httpContext.Request, arOptions);
723+
724+
// Should not be rejected due to token type
725+
Assert.NotEqual(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
726+
}
727+
728+
[Fact]
729+
public async Task TestM2MTokenRejectedWhenNotAccepted()
730+
{
731+
var arOptions = new AuthenticateRequestOptions(
732+
secretKey: "sk_test_secret",
733+
acceptsToken: new[] { "session_token" }
734+
);
735+
736+
var httpContext = CreateHttpContextWithToken("m2m_test_token");
737+
var state = await AuthenticateRequest.AuthenticateRequestAsync(httpContext.Request, arOptions);
738+
739+
Assert.True(state.IsSignedOut());
740+
Assert.Equal(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
741+
}
742+
743+
[Theory]
744+
[InlineData("m2m_test_token", new[] { "m2m_token" }, false)] // Should be accepted
745+
[InlineData("m2m_test_token", new[] { "machine_token" }, false)] // Should be accepted (machine_token includes m2m_token)
746+
[InlineData("m2m_test_token", new[] { "session_token" }, true)] // Should be rejected
747+
[InlineData("m2m_test_token", new[] { "oauth_token", "api_key" }, true)] // Should be rejected
748+
public async Task TestM2MTokenTypeFiltering(string token, string[] acceptedTypes, bool shouldBeRejected)
749+
{
750+
var arOptions = new AuthenticateRequestOptions(
751+
secretKey: "sk_test_secret",
752+
acceptsToken: acceptedTypes
753+
);
754+
755+
var httpContext = CreateHttpContextWithToken(token);
756+
var state = await AuthenticateRequest.AuthenticateRequestAsync(httpContext.Request, arOptions);
757+
758+
if (shouldBeRejected)
759+
{
760+
Assert.True(state.IsSignedOut());
761+
Assert.Equal(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
762+
}
763+
else
764+
{
765+
// Token type is accepted, but verification might still fail
766+
Assert.NotEqual(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED, state.ErrorReason);
767+
}
768+
}
769+
621770
#endregion
622771

623772
#region Error Handling Tests

0 commit comments

Comments
 (0)