diff --git a/src/Clerk/BackendAPI/Clerk.BackendAPI.csproj b/src/Clerk/BackendAPI/Clerk.BackendAPI.csproj index 21999d5..9ba888a 100644 --- a/src/Clerk/BackendAPI/Clerk.BackendAPI.csproj +++ b/src/Clerk/BackendAPI/Clerk.BackendAPI.csproj @@ -1,10 +1,10 @@ - + true Clerk.BackendAPI 0.13.0 - net8.0 + netstandard2.1;net8.0 Clerk Copyright (c) Clerk 2025 https://github.com/clerk/clerk-sdk-csharp.git diff --git a/src/Clerk/BackendAPI/Helpers/AuthObjects.cs b/src/Clerk/BackendAPI/Helpers/AuthObjects.cs index bc21456..779b78f 100644 --- a/src/Clerk/BackendAPI/Helpers/AuthObjects.cs +++ b/src/Clerk/BackendAPI/Helpers/AuthObjects.cs @@ -1,84 +1,85 @@ using System.Collections.Generic; -namespace Clerk.BackendAPI.Helpers.Jwks; - -/// -/// Abstract base class for all authentication objects -/// -public abstract class AuthObject +namespace Clerk.BackendAPI.Helpers.Jwks { -} + /// + /// Abstract base class for all authentication objects + /// + public abstract class AuthObject + { + } -/// -/// Session authentication object for version 2 tokens -/// -public class SessionAuthObjectV2 : AuthObject -{ - public string? Azp { get; set; } - public string? Email { get; set; } - public int Exp { get; set; } - public List? Fva { get; set; } - public int Iat { get; set; } - public string? Iss { get; set; } - public string? Jti { get; set; } - public int Nbf { get; set; } - public string? Role { get; set; } - public string? Sid { get; set; } - public string? Sub { get; set; } - public int V { get; set; } -} + /// + /// Session authentication object for version 2 tokens + /// + public class SessionAuthObjectV2 : AuthObject + { + public string? Azp { get; set; } + public string? Email { get; set; } + public int Exp { get; set; } + public List? Fva { get; set; } + public int Iat { get; set; } + public string? Iss { get; set; } + public string? Jti { get; set; } + public int Nbf { get; set; } + public string? Role { get; set; } + public string? Sid { get; set; } + public string? Sub { get; set; } + public int V { get; set; } + } -/// -/// Session authentication object for version 1 tokens -/// -public class SessionAuthObjectV1 : AuthObject -{ - public string? SessionId { get; set; } - public string? UserId { get; set; } - public string? OrgId { get; set; } - public string? OrgRole { get; set; } - public List? OrgPermissions { get; set; } - public List? FactorVerificationAge { get; set; } - public Dictionary? Claims { get; set; } -} + /// + /// Session authentication object for version 1 tokens + /// + public class SessionAuthObjectV1 : AuthObject + { + public string? SessionId { get; set; } + public string? UserId { get; set; } + public string? OrgId { get; set; } + public string? OrgRole { get; set; } + public List? OrgPermissions { get; set; } + public List? FactorVerificationAge { get; set; } + public Dictionary? Claims { get; set; } + } -/// -/// OAuth machine authentication object -/// -public class OAuthMachineAuthObject : AuthObject -{ - public TokenType TokenType { get; set; } = TokenType.OAuthToken; - public string? Id { get; set; } - public string? UserId { get; set; } - public string? ClientId { get; set; } - public string? Name { get; set; } - public List? Scopes { get; set; } -} + /// + /// OAuth machine authentication object + /// + public class OAuthMachineAuthObject : AuthObject + { + public TokenType TokenType { get; set; } = TokenType.OAuthToken; + public string? Id { get; set; } + public string? UserId { get; set; } + public string? ClientId { get; set; } + public string? Name { get; set; } + public List? Scopes { get; set; } + } -/// -/// API key machine authentication object -/// -public class APIKeyMachineAuthObject : AuthObject -{ - public TokenType TokenType { get; set; } = TokenType.ApiKey; - public string? Id { get; set; } - public string? UserId { get; set; } - public string? OrgId { get; set; } - public string? Name { get; set; } - public List? Scopes { get; set; } - public Dictionary? Claims { get; set; } -} + /// + /// API key machine authentication object + /// + public class APIKeyMachineAuthObject : AuthObject + { + public TokenType TokenType { get; set; } = TokenType.ApiKey; + public string? Id { get; set; } + public string? UserId { get; set; } + public string? OrgId { get; set; } + public string? Name { get; set; } + public List? Scopes { get; set; } + public Dictionary? Claims { get; set; } + } -/// -/// M2M machine authentication object -/// -public class M2MMachineAuthObject : AuthObject -{ - public TokenType TokenType { get; set; } = TokenType.MachineToken; - public string? Id { get; set; } - public string? MachineId { get; set; } - public string? ClientId { get; set; } - public string? Name { get; set; } - public List? Scopes { get; set; } - public Dictionary? Claims { get; set; } + /// + /// M2M machine authentication object + /// + public class M2MMachineAuthObject : AuthObject + { + public TokenType TokenType { get; set; } = TokenType.MachineToken; + public string? Id { get; set; } + public string? MachineId { get; set; } + public string? ClientId { get; set; } + public string? Name { get; set; } + public List? Scopes { get; set; } + public Dictionary? Claims { get; set; } + } } \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Helpers/AuthenticateRequest.cs b/src/Clerk/BackendAPI/Helpers/AuthenticateRequest.cs index ddf2df6..3be8be6 100644 --- a/src/Clerk/BackendAPI/Helpers/AuthenticateRequest.cs +++ b/src/Clerk/BackendAPI/Helpers/AuthenticateRequest.cs @@ -8,134 +8,136 @@ -namespace Clerk.BackendAPI.Helpers.Jwks; - -/// -/// AuthenticateRequest - Helper methods to authenticate requests. -/// -public static class AuthenticateRequest +namespace Clerk.BackendAPI.Helpers.Jwks { - private const string SESSION_COOKIE_PREFIX = "__session"; /// - /// Checks if the HTTP request is authenticated. - /// First the session token is retrieved from either the __session cookie - /// or the HTTP Authorization header. - /// Then the session token is verified: networklessly if the options.jwtKey - /// is provided, otherwise by fetching the JWKS from Clerk's Backend API. + /// AuthenticateRequest - Helper methods to authenticate requests. /// - /// The HTTP request - /// The request authentication options - /// The request state - /// WARNING: AuthenticateRequestAsync is applicable in the context of Backend APIs only. - public static async Task AuthenticateRequestAsync( - HttpRequest request, - AuthenticateRequestOptions options) + public static class AuthenticateRequest { - var sessionToken = GetSessionToken(request); - if (sessionToken == null) return RequestState.SignedOut(AuthErrorReason.SESSION_TOKEN_MISSING); + private const string SESSION_COOKIE_PREFIX = "__session"; - var tokenType = TokenTypeHelper.GetTokenType(sessionToken); - var tokenTypeName = tokenType switch + /// + /// Checks if the HTTP request is authenticated. + /// First the session token is retrieved from either the __session cookie + /// or the HTTP Authorization header. + /// Then the session token is verified: networklessly if the options.jwtKey + /// is provided, otherwise by fetching the JWKS from Clerk's Backend API. + /// + /// The HTTP request + /// The request authentication options + /// The request state + /// WARNING: AuthenticateRequestAsync is applicable in the context of Backend APIs only. + public static async Task AuthenticateRequestAsync( + HttpRequest request, + AuthenticateRequestOptions options) { - TokenType.SessionToken => "session_token", - TokenType.MachineToken => "machine_token", - TokenType.MachineTokenV2 => "m2m_token", - TokenType.OAuthToken => "oauth_token", - TokenType.ApiKey => "api_key", - _ => tokenType.ToString().ToLowerInvariant() - }; + var sessionToken = GetSessionToken(request); + if (sessionToken == null) return RequestState.SignedOut(AuthErrorReason.SESSION_TOKEN_MISSING); - // Check if token type is accepted - if (!options.AcceptsToken.Contains("any") && !options.AcceptsToken.Contains(tokenTypeName)) - { - // Special case: if acceptsToken contains "machine_token", accept both MachineToken and MachineTokenV2 - bool isAccepted = false; - if (options.AcceptsToken.Contains("machine_token") && - (tokenType == TokenType.MachineToken || tokenType == TokenType.MachineTokenV2)) + var tokenType = TokenTypeHelper.GetTokenType(sessionToken); + var tokenTypeName = tokenType switch { - isAccepted = true; - } + TokenType.SessionToken => "session_token", + TokenType.MachineToken => "machine_token", + TokenType.MachineTokenV2 => "m2m_token", + TokenType.OAuthToken => "oauth_token", + TokenType.ApiKey => "api_key", + _ => tokenType.ToString().ToLowerInvariant() + }; - if (!isAccepted) + // Check if token type is accepted + if (!options.AcceptsToken.Contains("any") && !options.AcceptsToken.Contains(tokenTypeName)) { - return RequestState.SignedOut(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED); + // Special case: if acceptsToken contains "machine_token", accept both MachineToken and MachineTokenV2 + bool isAccepted = false; + if (options.AcceptsToken.Contains("machine_token") && + (tokenType == TokenType.MachineToken || tokenType == TokenType.MachineTokenV2)) + { + isAccepted = true; + } + + if (!isAccepted) + { + return RequestState.SignedOut(AuthErrorReason.TOKEN_TYPE_NOT_SUPPORTED); + } } - } - VerifyTokenOptions verifyTokenOptions; + VerifyTokenOptions verifyTokenOptions; - if (TokenTypeHelper.IsMachineToken(sessionToken)) - { - if (options.SecretKey == null && options.MachineSecretKey == null) - return RequestState.SignedOut(AuthErrorReason.SECRET_KEY_MISSING); + if (TokenTypeHelper.IsMachineToken(sessionToken)) + { + if (options.SecretKey == null && options.MachineSecretKey == null) + return RequestState.SignedOut(AuthErrorReason.SECRET_KEY_MISSING); - verifyTokenOptions = new VerifyTokenOptions( - secretKey: options.SecretKey, - machineSecretKey: options.MachineSecretKey - ); - } - else - { - // Session tokens can use either JWT key or secret key - if (options.JwtKey != null) - verifyTokenOptions = new VerifyTokenOptions( - jwtKey: options.JwtKey, - audiences: options.Audiences, - authorizedParties: options.AuthorizedParties, - clockSkewInMs: options.ClockSkewInMs - ); - else if (options.SecretKey != null) verifyTokenOptions = new VerifyTokenOptions( - options.SecretKey, - audiences: options.Audiences, - authorizedParties: options.AuthorizedParties, - clockSkewInMs: options.ClockSkewInMs + secretKey: options.SecretKey, + machineSecretKey: options.MachineSecretKey ); + } else - return RequestState.SignedOut(AuthErrorReason.SECRET_KEY_MISSING); - } + { + // Session tokens can use either JWT key or secret key + if (options.JwtKey != null) + verifyTokenOptions = new VerifyTokenOptions( + jwtKey: options.JwtKey, + audiences: options.Audiences, + authorizedParties: options.AuthorizedParties, + clockSkewInMs: options.ClockSkewInMs + ); + else if (options.SecretKey != null) + verifyTokenOptions = new VerifyTokenOptions( + options.SecretKey, + audiences: options.Audiences, + authorizedParties: options.AuthorizedParties, + clockSkewInMs: options.ClockSkewInMs + ); + else + return RequestState.SignedOut(AuthErrorReason.SECRET_KEY_MISSING); + } - try - { - var claims = await VerifyToken.VerifyTokenAsync(sessionToken, verifyTokenOptions); - return RequestState.SignedIn(sessionToken, claims); - } - catch (TokenVerificationException e) - { - return RequestState.SignedOut(e.Reason); + try + { + var claims = await VerifyToken.VerifyTokenAsync(sessionToken, verifyTokenOptions); + return RequestState.SignedIn(sessionToken, claims); + } + catch (TokenVerificationException e) + { + return RequestState.SignedOut(e.Reason); + } } - } - /// - /// Retrieve token from __session cookie or Authorization header. - /// - /// The HTTP request - /// The session token, if present - private static string? GetSessionToken(HttpRequest request) - { - var authorizationHeaders = request.Headers.GetCommaSeparatedValues("Authorization"); - if (authorizationHeaders != StringValues.Empty) - { - var bearerToken = authorizationHeaders.FirstOrDefault(); - if (!string.IsNullOrEmpty(bearerToken)) return bearerToken.Replace("Bearer ", ""); - } - var cookieHeaders = request.Headers.GetCommaSeparatedValues("Cookie"); - if (cookieHeaders != StringValues.Empty) + /// + /// Retrieve token from __session cookie or Authorization header. + /// + /// The HTTP request + /// The session token, if present + private static string? GetSessionToken(HttpRequest request) { - var cookieHeaderValue = cookieHeaders.FirstOrDefault(); - if (!string.IsNullOrEmpty(cookieHeaderValue)) + var authorizationHeaders = request.Headers.GetCommaSeparatedValues("Authorization"); + if (authorizationHeaders != StringValues.Empty) { - var cookies = cookieHeaderValue.Split(';') - .Select(cookie => cookie.Trim()) - .Select(cookie => new Cookie(cookie.Split('=')[0], cookie.Split('=')[1])); + var bearerToken = authorizationHeaders.FirstOrDefault(); + if (!string.IsNullOrEmpty(bearerToken)) return bearerToken.Replace("Bearer ", ""); + } + var cookieHeaders = request.Headers.GetCommaSeparatedValues("Cookie"); + if (cookieHeaders != StringValues.Empty) + { + var cookieHeaderValue = cookieHeaders.FirstOrDefault(); + if (!string.IsNullOrEmpty(cookieHeaderValue)) + { + var cookies = cookieHeaderValue.Split(';') + .Select(cookie => cookie.Trim()) + .Select(cookie => new Cookie(cookie.Split('=')[0], cookie.Split('=')[1])); - foreach (var cookie in cookies) - if (cookie.Name.StartsWith(SESSION_COOKIE_PREFIX)) - return cookie.Value; + foreach (var cookie in cookies) + if (cookie.Name.StartsWith(SESSION_COOKIE_PREFIX)) + return cookie.Value; + } } - } - return null; + return null; + } } } \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Helpers/AuthenticateRequestException.cs b/src/Clerk/BackendAPI/Helpers/AuthenticateRequestException.cs index 50779b3..6305023 100644 --- a/src/Clerk/BackendAPI/Helpers/AuthenticateRequestException.cs +++ b/src/Clerk/BackendAPI/Helpers/AuthenticateRequestException.cs @@ -1,40 +1,42 @@ using System; -namespace Clerk.BackendAPI.Helpers.Jwks; - -public static class AuthErrorReason -{ - public static readonly ErrorReason - SESSION_TOKEN_MISSING = new( - "session-token-missing", - "Could not retrieve session token. Please make sure that the __session cookie or the HTTP authorization header contain a Clerk-generated session JWT" - ), - SECRET_KEY_MISSING = new( - "secret-key-missing", - "Missing Clerk Secret Key. Go to https://dashboard.clerk.com and get your key for your instance." - ), - TOKEN_TYPE_NOT_SUPPORTED = new( - "token-type-not-supported", - "The provided token type is not supported. Expected one of: session_token, machine_token, oauth_token, or api_key." - ); -} - -public class AuthenticateRequestException : Exception +namespace Clerk.BackendAPI.Helpers.Jwks { - public readonly ErrorReason Reason; - public AuthenticateRequestException(ErrorReason reason) : base(reason.Message) + public static class AuthErrorReason { - Reason = reason; + public static readonly ErrorReason + SESSION_TOKEN_MISSING = new ErrorReason( + "session-token-missing", + "Could not retrieve session token. Please make sure that the __session cookie or the HTTP authorization header contain a Clerk-generated session JWT" + ), + SECRET_KEY_MISSING = new ErrorReason( + "secret-key-missing", + "Missing Clerk Secret Key. Go to https://dashboard.clerk.com and get your key for your instance." + ), + TOKEN_TYPE_NOT_SUPPORTED = new ErrorReason( + "token-type-not-supported", + "The provided token type is not supported. Expected one of: session_token, machine_token, oauth_token, or api_key." + ); } - public AuthenticateRequestException(ErrorReason reason, Exception cause) : base(reason.Message, cause) + public class AuthenticateRequestException : Exception { - Reason = reason; - } + public readonly ErrorReason Reason; - public override string ToString() - { - return Reason.Message; + public AuthenticateRequestException(ErrorReason reason) : base(reason.Message) + { + Reason = reason; + } + + public AuthenticateRequestException(ErrorReason reason, Exception cause) : base(reason.Message, cause) + { + Reason = reason; + } + + public override string ToString() + { + return Reason.Message; + } } } \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Helpers/AuthenticateRequestOptions.cs b/src/Clerk/BackendAPI/Helpers/AuthenticateRequestOptions.cs index bbf9d66..1a8e8bc 100644 --- a/src/Clerk/BackendAPI/Helpers/AuthenticateRequestOptions.cs +++ b/src/Clerk/BackendAPI/Helpers/AuthenticateRequestOptions.cs @@ -1,50 +1,50 @@ using System.Collections.Generic; -using System.Linq; -namespace Clerk.BackendAPI.Helpers.Jwks; - -public sealed class AuthenticateRequestOptions +namespace Clerk.BackendAPI.Helpers.Jwks { - private static readonly long DEFAULT_CLOCK_SKEW_MS = 5000L; - public readonly IEnumerable? Audiences; - public readonly IEnumerable AuthorizedParties; - public readonly long ClockSkewInMs; - public readonly string? JwtKey; - public readonly string? SecretKey; - public readonly string? MachineSecretKey; - public readonly IEnumerable AcceptsToken; - - /// - /// Options to configure AuthenticateRequestAsync. - /// - /// The Clerk secret key from the API Keys page in the Clerk Dashboard. (Optional) - /// The Machine secret key for machine-specific authentication. (Optional) - /// PEM Public String used to verify the session token in a networkless manner. (Optional) - /// A list of audiences to verify against. - /// An allowlist of origins to verify against. - /// - /// Allowed time difference (in milliseconds) between the Clerk server (which generates the - /// token) and the user's application server when validating a token. Defaults to 5000 ms. - /// - /// A list of token types to accept. Defaults to ["any"]. - public AuthenticateRequestOptions( - string? secretKey = null, - string? machineSecretKey = null, - string? jwtKey = null, - IEnumerable? audiences = null, - IEnumerable? authorizedParties = null, - long? clockSkewInMs = null, - IEnumerable? acceptsToken = null) + public sealed class AuthenticateRequestOptions { - if (string.IsNullOrEmpty(secretKey) && string.IsNullOrEmpty(jwtKey) && string.IsNullOrEmpty(machineSecretKey)) - throw new AuthenticateRequestException(AuthErrorReason.SECRET_KEY_MISSING); + private static readonly long DEFAULT_CLOCK_SKEW_MS = 5000L; + public readonly IEnumerable? Audiences; + public readonly IEnumerable AuthorizedParties; + public readonly long ClockSkewInMs; + public readonly string? JwtKey; + public readonly string? SecretKey; + public readonly string? MachineSecretKey; + public readonly IEnumerable AcceptsToken; + + /// + /// Options to configure AuthenticateRequestAsync. + /// + /// The Clerk secret key from the API Keys page in the Clerk Dashboard. (Optional) + /// The Machine secret key for machine-specific authentication. (Optional) + /// PEM Public String used to verify the session token in a networkless manner. (Optional) + /// A list of audiences to verify against. + /// An allowlist of origins to verify against. + /// + /// Allowed time difference (in milliseconds) between the Clerk server (which generates the + /// token) and the user's application server when validating a token. Defaults to 5000 ms. + /// + /// A list of token types to accept. Defaults to ["any"]. + public AuthenticateRequestOptions( + string? secretKey = null, + string? machineSecretKey = null, + string? jwtKey = null, + IEnumerable? audiences = null, + IEnumerable? authorizedParties = null, + long? clockSkewInMs = null, + IEnumerable? acceptsToken = null) + { + if (string.IsNullOrEmpty(secretKey) && string.IsNullOrEmpty(jwtKey) && string.IsNullOrEmpty(machineSecretKey)) + throw new AuthenticateRequestException(AuthErrorReason.SECRET_KEY_MISSING); - SecretKey = secretKey; - MachineSecretKey = machineSecretKey; - JwtKey = jwtKey; - Audiences = audiences; - AuthorizedParties = authorizedParties ?? new List(); - ClockSkewInMs = clockSkewInMs ?? DEFAULT_CLOCK_SKEW_MS; - AcceptsToken = acceptsToken ?? new[] { "any" }; + SecretKey = secretKey; + MachineSecretKey = machineSecretKey; + JwtKey = jwtKey; + Audiences = audiences; + AuthorizedParties = authorizedParties ?? new List(); + ClockSkewInMs = clockSkewInMs ?? DEFAULT_CLOCK_SKEW_MS; + AcceptsToken = acceptsToken ?? new[] { "any" }; + } } -} +} \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Helpers/ErrorReason.cs b/src/Clerk/BackendAPI/Helpers/ErrorReason.cs index 24af0fa..e26838c 100644 --- a/src/Clerk/BackendAPI/Helpers/ErrorReason.cs +++ b/src/Clerk/BackendAPI/Helpers/ErrorReason.cs @@ -1,16 +1,17 @@ -namespace Clerk.BackendAPI.Helpers.Jwks; - -/// -/// Represents the reason for a TokenVerificationException or AuthenticateRequestException. -/// -public class ErrorReason +namespace Clerk.BackendAPI.Helpers.Jwks { - public readonly string Id; - public readonly string Message; - - public ErrorReason(string id, string message) + /// + /// Represents the reason for a TokenVerificationException or AuthenticateRequestException. + /// + public class ErrorReason { - Id = id; - Message = message; + public readonly string Id; + public readonly string Message; + + public ErrorReason(string id, string message) + { + Id = id; + Message = message; + } } } \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Helpers/OrganizationClaimsProcessor.cs b/src/Clerk/BackendAPI/Helpers/OrganizationClaimsProcessor.cs index 7985676..f43eed8 100644 --- a/src/Clerk/BackendAPI/Helpers/OrganizationClaimsProcessor.cs +++ b/src/Clerk/BackendAPI/Helpers/OrganizationClaimsProcessor.cs @@ -3,78 +3,78 @@ using System.Linq; using System.Security.Claims; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -namespace Clerk.BackendAPI.Helpers.Jwks; - -public static class OrganizationClaimsProcessor +namespace Clerk.BackendAPI.Helpers.Jwks { - public static ClaimsPrincipal ProcessOrganizationClaims(ClaimsPrincipal claims) + public static class OrganizationClaimsProcessor { - var claimsIdentity = claims.Identity as ClaimsIdentity; - if (claimsIdentity == null) return claims; + public static ClaimsPrincipal ProcessOrganizationClaims(ClaimsPrincipal claims) + { + var claimsIdentity = claims.Identity as ClaimsIdentity; + if (claimsIdentity == null) return claims; - var version = claims.FindFirst("v")?.Value; - if (version != "2") return claims; + var version = claims.FindFirst("v")?.Value; + if (version != "2") return claims; - var orgClaim = claims.FindFirst("o"); - if (orgClaim == null) return claims; + var orgClaim = claims.FindFirst("o"); + if (orgClaim == null) return claims; - var orgClaims = JsonConvert.DeserializeObject>(orgClaim.Value); - if (orgClaims == null) return claims; + var orgClaims = JsonConvert.DeserializeObject>(orgClaim.Value); + if (orgClaims == null) return claims; - if (orgClaims.TryGetValue("id", out var orgId)) - claimsIdentity.AddClaim(new Claim("org_id", orgId.ToString() ?? string.Empty)); - if (orgClaims.TryGetValue("slg", out var orgSlug)) - claimsIdentity.AddClaim(new Claim("org_slug", orgSlug.ToString() ?? string.Empty)); + if (orgClaims.TryGetValue("id", out var orgId)) + claimsIdentity.AddClaim(new Claim("org_id", orgId.ToString() ?? string.Empty)); + if (orgClaims.TryGetValue("slg", out var orgSlug)) + claimsIdentity.AddClaim(new Claim("org_slug", orgSlug.ToString() ?? string.Empty)); - if (orgClaims.TryGetValue("rol", out var orgRole)) - { - claimsIdentity.AddClaim(new Claim("org_role", orgRole.ToString() ?? string.Empty)); - } + if (orgClaims.TryGetValue("rol", out var orgRole)) + { + claimsIdentity.AddClaim(new Claim("org_role", orgRole.ToString() ?? string.Empty)); + } - var features = claims.FindFirst("fea")?.Value?.Split(','); - var permissions = orgClaims.TryGetValue("per", out var per) ? per.ToString()?.Split(',') : null; - var mappings = orgClaims.TryGetValue("fpm", out var fpm) ? fpm.ToString()?.Split(',') : null; + var features = claims.FindFirst("fea")?.Value?.Split(','); + var permissions = orgClaims.TryGetValue("per", out var per) ? per.ToString()?.Split(',') : null; + var mappings = orgClaims.TryGetValue("fpm", out var fpm) ? fpm.ToString()?.Split(',') : null; - if (features != null && permissions != null && mappings != null) - { - var orgPermissions = ComputeOrgPermissions(features, permissions, mappings); - if (orgPermissions.Any()) + if (features != null && permissions != null && mappings != null) { - claimsIdentity.AddClaim(new Claim("org_permissions", string.Join(",", orgPermissions))); + var orgPermissions = ComputeOrgPermissions(features, permissions, mappings); + if (orgPermissions.Any()) + { + claimsIdentity.AddClaim(new Claim("org_permissions", string.Join(",", orgPermissions))); + } } - } - return claims; - } - - private static IEnumerable ComputeOrgPermissions(string[] features, string[] permissions, string[] mappings) - { - var orgPermissions = new List(); + return claims; + } - for (int idx = 0; idx < mappings.Length; idx++) + private static IEnumerable ComputeOrgPermissions(string[] features, string[] permissions, string[] mappings) { - var mapping = mappings[idx]; - var featureParts = features[idx].Split(':'); - if (featureParts.Length != 2) continue; + var orgPermissions = new List(); - var scope = featureParts[0]; - var feature = featureParts[1]; - if (!scope.Contains("o")) continue; + for (int idx = 0; idx < mappings.Length; idx++) + { + var mapping = mappings[idx]; + var featureParts = features[idx].Split(':'); + if (featureParts.Length != 2) continue; - var binary = Convert.ToString(int.Parse(mapping), 2).TrimStart('0'); - var reversedBinary = new string(binary.Reverse().ToArray()); + var scope = featureParts[0]; + var feature = featureParts[1]; + if (!scope.Contains("o")) continue; - for (int i = 0; i < reversedBinary.Length; i++) - { - if (reversedBinary[i] == '1' && i < permissions.Length) + var binary = Convert.ToString(int.Parse(mapping), 2).TrimStart('0'); + var reversedBinary = new string(binary.Reverse().ToArray()); + + for (int i = 0; i < reversedBinary.Length; i++) { - orgPermissions.Add($"org:{feature}:{permissions[i]}"); + if (reversedBinary[i] == '1' && i < permissions.Length) + { + orgPermissions.Add($"org:{feature}:{permissions[i]}"); + } } } - } - return orgPermissions; + return orgPermissions; + } } } \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Helpers/RequestState.cs b/src/Clerk/BackendAPI/Helpers/RequestState.cs index 6d9a399..ba65f7c 100644 --- a/src/Clerk/BackendAPI/Helpers/RequestState.cs +++ b/src/Clerk/BackendAPI/Helpers/RequestState.cs @@ -2,144 +2,145 @@ using System.Linq; using System.Security.Claims; -namespace Clerk.BackendAPI.Helpers.Jwks; - -/// -/// AuthStatus - The request authentication status. -/// -public class AuthStatus +namespace Clerk.BackendAPI.Helpers.Jwks { - public static readonly AuthStatus SignedIn = new("signed-in"); - public static readonly AuthStatus SignedOut = new("signed-out"); - - private readonly string value; - - private AuthStatus(string value) + /// + /// AuthStatus - The request authentication status. + /// + public class AuthStatus { - this.value = value; - } + public static readonly AuthStatus SignedIn = new AuthStatus("signed-in"); + public static readonly AuthStatus SignedOut = new AuthStatus("signed-out"); - public string Value() - { - return value; - } -} - -/// -/// RequestState - Authentication State of the request. -/// -public class RequestState -{ - public readonly ClaimsPrincipal? Claims; - public readonly ErrorReason? ErrorReason; - public readonly AuthStatus Status; - public readonly string? Token; + private readonly string value; + private AuthStatus(string value) + { + this.value = value; + } - public RequestState(AuthStatus status, - ErrorReason? errorReason, - string? token, - ClaimsPrincipal? claims) - { - Status = status; - ErrorReason = errorReason; - Token = token; - Claims = claims; + public string Value() + { + return value; + } } - public static RequestState SignedIn(string token, ClaimsPrincipal claims) + /// + /// RequestState - Authentication State of the request. + /// + public class RequestState { - return new RequestState(AuthStatus.SignedIn, null, token, claims); - } + public readonly ClaimsPrincipal? Claims; + public readonly ErrorReason? ErrorReason; + public readonly AuthStatus Status; + public readonly string? Token; - public static RequestState SignedOut(ErrorReason errorReason) - { - return new RequestState(AuthStatus.SignedOut, errorReason, null, null); - } - public bool IsAuthenticated => Status == AuthStatus.SignedIn; + public RequestState(AuthStatus status, + ErrorReason? errorReason, + string? token, + ClaimsPrincipal? claims) + { + Status = status; + ErrorReason = errorReason; + Token = token; + Claims = claims; + } - [Obsolete("Use IsAuthenticated instead.")] - public bool IsSignedIn() - { - return Status == AuthStatus.SignedIn; - } + public static RequestState SignedIn(string token, ClaimsPrincipal claims) + { + return new RequestState(AuthStatus.SignedIn, null, token, claims); + } - public bool IsSignedOut() - { - return Status == AuthStatus.SignedOut; - } + public static RequestState SignedOut(ErrorReason errorReason) + { + return new RequestState(AuthStatus.SignedOut, errorReason, null, null); + } - public AuthObject ToAuth() - { - if (Status != AuthStatus.SignedIn || Claims == null) - throw new InvalidOperationException("Cannot convert to auth object when not signed in."); + public bool IsAuthenticated => Status == AuthStatus.SignedIn; + + [Obsolete("Use IsAuthenticated instead.")] + public bool IsSignedIn() + { + return Status == AuthStatus.SignedIn; + } + + public bool IsSignedOut() + { + return Status == AuthStatus.SignedOut; + } - var tokenType = TokenTypeHelper.GetTokenType(Token); - switch (tokenType) + public AuthObject ToAuth() { - case TokenType.SessionToken: - var versionClaim = Claims.FindFirst("v")?.Value; - if (versionClaim == "2") - { - return new SessionAuthObjectV2 + if (Status != AuthStatus.SignedIn || Claims == null) + throw new InvalidOperationException("Cannot convert to auth object when not signed in."); + + var tokenType = TokenTypeHelper.GetTokenType(Token); + switch (tokenType) + { + case TokenType.SessionToken: + var versionClaim = Claims.FindFirst("v")?.Value; + if (versionClaim == "2") + { + return new SessionAuthObjectV2 + { + Azp = Claims.FindFirst("azp")?.Value, + Email = Claims.FindFirst("email")?.Value, + Exp = int.Parse(Claims.FindFirst("exp")?.Value ?? "0"), + Fva = Claims.FindAll("fva").Select(c => int.Parse(c.Value)).ToList(), + Iat = int.Parse(Claims.FindFirst("iat")?.Value ?? "0"), + Iss = Claims.FindFirst("iss")?.Value, + Jti = Claims.FindFirst("jti")?.Value, + Nbf = int.Parse(Claims.FindFirst("nbf")?.Value ?? "0"), + Role = Claims.FindFirst("role")?.Value, + Sid = Claims.FindFirst("sid")?.Value, + Sub = Claims.FindFirst("sub")?.Value, + V = int.Parse(versionClaim) + }; + } + return new SessionAuthObjectV1 + { + SessionId = Claims.FindFirst("sid")?.Value, + UserId = Claims.FindFirst("sub")?.Value, + OrgId = Claims.FindFirst("org_id")?.Value, + OrgRole = Claims.FindFirst("org_role")?.Value, + OrgPermissions = Claims.FindAll("org_permissions").Select(c => c.Value).ToList(), + FactorVerificationAge = Claims.FindAll("fva").Select(c => int.Parse(c.Value)).ToList(), + Claims = Claims.Claims.GroupBy(c => c.Type).ToDictionary(g => g.Key, g => (object)g.Select(c => c.Value).ToList()) + }; + case TokenType.OAuthToken: + return new OAuthMachineAuthObject + { + Id = Claims.FindFirst("id")?.Value, + UserId = Claims.FindFirst("subject")?.Value, + ClientId = Claims.FindFirst("client_id")?.Value, + Name = Claims.FindFirst("name")?.Value, + Scopes = Claims.FindAll("scopes").Select(c => c.Value).ToList() + }; + case TokenType.ApiKey: + return new APIKeyMachineAuthObject + { + Id = Claims.FindFirst("id")?.Value, + UserId = Claims.FindFirst("subject")?.Value, + OrgId = Claims.FindFirst("org_id")?.Value, + Name = Claims.FindFirst("name")?.Value, + Scopes = Claims.FindAll("scopes").Select(c => c.Value).ToList(), + Claims = Claims.Claims.GroupBy(c => c.Type).ToDictionary(g => g.Key, g => (object)g.Select(c => c.Value).ToList()) + }; + case TokenType.MachineToken: + case TokenType.MachineTokenV2: + return new M2MMachineAuthObject { - Azp = Claims.FindFirst("azp")?.Value, - Email = Claims.FindFirst("email")?.Value, - Exp = int.Parse(Claims.FindFirst("exp")?.Value ?? "0"), - Fva = Claims.FindAll("fva").Select(c => int.Parse(c.Value)).ToList(), - Iat = int.Parse(Claims.FindFirst("iat")?.Value ?? "0"), - Iss = Claims.FindFirst("iss")?.Value, - Jti = Claims.FindFirst("jti")?.Value, - Nbf = int.Parse(Claims.FindFirst("nbf")?.Value ?? "0"), - Role = Claims.FindFirst("role")?.Value, - Sid = Claims.FindFirst("sid")?.Value, - Sub = Claims.FindFirst("sub")?.Value, - V = int.Parse(versionClaim) + Id = Claims.FindFirst("id")?.Value, + MachineId = Claims.FindFirst("subject")?.Value, + ClientId = Claims.FindFirst("client_id")?.Value, + Name = Claims.FindFirst("name")?.Value, + Scopes = Claims.FindAll("scopes").Select(c => c.Value).ToList(), + Claims = Claims.Claims.GroupBy(c => c.Type).ToDictionary(g => g.Key, g => (object)g.Select(c => c.Value).ToList()) }; - } - return new SessionAuthObjectV1 - { - SessionId = Claims.FindFirst("sid")?.Value, - UserId = Claims.FindFirst("sub")?.Value, - OrgId = Claims.FindFirst("org_id")?.Value, - OrgRole = Claims.FindFirst("org_role")?.Value, - OrgPermissions = Claims.FindAll("org_permissions").Select(c => c.Value).ToList(), - FactorVerificationAge = Claims.FindAll("fva").Select(c => int.Parse(c.Value)).ToList(), - Claims = Claims.Claims.GroupBy(c => c.Type).ToDictionary(g => g.Key, g => (object)g.Select(c => c.Value).ToList()) - }; - case TokenType.OAuthToken: - return new OAuthMachineAuthObject - { - Id = Claims.FindFirst("id")?.Value, - UserId = Claims.FindFirst("subject")?.Value, - ClientId = Claims.FindFirst("client_id")?.Value, - Name = Claims.FindFirst("name")?.Value, - Scopes = Claims.FindAll("scopes").Select(c => c.Value).ToList() - }; - case TokenType.ApiKey: - return new APIKeyMachineAuthObject - { - Id = Claims.FindFirst("id")?.Value, - UserId = Claims.FindFirst("subject")?.Value, - OrgId = Claims.FindFirst("org_id")?.Value, - Name = Claims.FindFirst("name")?.Value, - Scopes = Claims.FindAll("scopes").Select(c => c.Value).ToList(), - Claims = Claims.Claims.GroupBy(c => c.Type).ToDictionary(g => g.Key, g => (object)g.Select(c => c.Value).ToList()) - }; - case TokenType.MachineToken: - case TokenType.MachineTokenV2: - return new M2MMachineAuthObject - { - Id = Claims.FindFirst("id")?.Value, - MachineId = Claims.FindFirst("subject")?.Value, - ClientId = Claims.FindFirst("client_id")?.Value, - Name = Claims.FindFirst("name")?.Value, - Scopes = Claims.FindAll("scopes").Select(c => c.Value).ToList(), - Claims = Claims.Claims.GroupBy(c => c.Type).ToDictionary(g => g.Key, g => (object)g.Select(c => c.Value).ToList()) - }; - default: - throw new InvalidOperationException($"Unsupported token type: {tokenType}"); + default: + throw new InvalidOperationException($"Unsupported token type: {tokenType}"); + } } } } \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Helpers/TokenTypes.cs b/src/Clerk/BackendAPI/Helpers/TokenTypes.cs index a360e6e..37a29c1 100644 --- a/src/Clerk/BackendAPI/Helpers/TokenTypes.cs +++ b/src/Clerk/BackendAPI/Helpers/TokenTypes.cs @@ -1,95 +1,96 @@ using System; -namespace Clerk.BackendAPI.Helpers.Jwks; - -/// -/// Represents different types of Clerk tokens -/// -public enum TokenType -{ - SessionToken, - MachineToken, - MachineTokenV2, - OAuthToken, - ApiKey -} - -/// -/// Token prefixes used to identify token types -/// -public static class TokenPrefix -{ - public const string MachineToken = "mt_"; - public const string MachineTokenV2 = "m2m_"; - public const string OAuthToken = "oat_"; - public const string ApiKey = "ak_"; -} - -/// -/// Helper methods for token type detection and classification -/// -public static class TokenTypeHelper +namespace Clerk.BackendAPI.Helpers.Jwks { - private static readonly string[] MachineTokenPrefixes = { TokenPrefix.MachineToken, TokenPrefix.MachineTokenV2, TokenPrefix.OAuthToken, TokenPrefix.ApiKey }; - /// - /// Determines if a token is a machine token (includes M2M, OAuth, and API key tokens) + /// Represents different types of Clerk tokens /// - /// The token to check - /// True if the token is a machine token - public static bool IsMachineToken(string token) + public enum TokenType { - if (string.IsNullOrEmpty(token)) - return false; - - foreach (var prefix in MachineTokenPrefixes) - { - if (token.StartsWith(prefix)) - return true; - } + SessionToken, + MachineToken, + MachineTokenV2, + OAuthToken, + ApiKey + } - return false; + /// + /// Token prefixes used to identify token types + /// + public static class TokenPrefix + { + public const string MachineToken = "mt_"; + public const string MachineTokenV2 = "m2m_"; + public const string OAuthToken = "oat_"; + public const string ApiKey = "ak_"; } /// - /// Gets the token type based on its prefix + /// Helper methods for token type detection and classification /// - /// The token to analyze - /// The detected token type - public static TokenType GetTokenType(string token) + public static class TokenTypeHelper { - if (string.IsNullOrEmpty(token)) - return TokenType.SessionToken; + private static readonly string[] MachineTokenPrefixes = { TokenPrefix.MachineToken, TokenPrefix.MachineTokenV2, TokenPrefix.OAuthToken, TokenPrefix.ApiKey }; - if (token.StartsWith(TokenPrefix.MachineToken)) - return TokenType.MachineToken; + /// + /// Determines if a token is a machine token (includes M2M, OAuth, and API key tokens) + /// + /// The token to check + /// True if the token is a machine token + public static bool IsMachineToken(string token) + { + if (string.IsNullOrEmpty(token)) + return false; - if (token.StartsWith(TokenPrefix.MachineTokenV2)) - return TokenType.MachineTokenV2; + foreach (var prefix in MachineTokenPrefixes) + { + if (token.StartsWith(prefix)) + return true; + } - if (token.StartsWith(TokenPrefix.ApiKey)) - return TokenType.ApiKey; + return false; + } - if (token.StartsWith(TokenPrefix.OAuthToken)) - return TokenType.OAuthToken; + /// + /// Gets the token type based on its prefix + /// + /// The token to analyze + /// The detected token type + public static TokenType GetTokenType(string token) + { + if (string.IsNullOrEmpty(token)) + return TokenType.SessionToken; - return TokenType.SessionToken; - } + if (token.StartsWith(TokenPrefix.MachineToken)) + return TokenType.MachineToken; - /// - /// Gets the verification endpoint for a given token type - /// - /// The token type - /// The API endpoint for verification - public static string GetVerificationEndpoint(TokenType tokenType) - { - return tokenType switch + if (token.StartsWith(TokenPrefix.MachineTokenV2)) + return TokenType.MachineTokenV2; + + if (token.StartsWith(TokenPrefix.ApiKey)) + return TokenType.ApiKey; + + if (token.StartsWith(TokenPrefix.OAuthToken)) + return TokenType.OAuthToken; + + return TokenType.SessionToken; + } + + /// + /// Gets the verification endpoint for a given token type + /// + /// The token type + /// The API endpoint for verification + public static string GetVerificationEndpoint(TokenType tokenType) { - TokenType.MachineToken => "/m2m_tokens/verify", - TokenType.MachineTokenV2 => "/m2m_tokens/verify", - TokenType.OAuthToken => "/oauth_applications/access_tokens/verify", - TokenType.ApiKey => "/api_keys/verify", - _ => throw new ArgumentException($"No verification endpoint for token type: {tokenType}") - }; + return tokenType switch + { + TokenType.MachineToken => "/m2m_tokens/verify", + TokenType.MachineTokenV2 => "/m2m_tokens/verify", + TokenType.OAuthToken => "/oauth_applications/access_tokens/verify", + TokenType.ApiKey => "/api_keys/verify", + _ => throw new ArgumentException($"No verification endpoint for token type: {tokenType}") + }; + } } } \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Helpers/TokenVerificationException.cs b/src/Clerk/BackendAPI/Helpers/TokenVerificationException.cs index ee45016..94607b4 100644 --- a/src/Clerk/BackendAPI/Helpers/TokenVerificationException.cs +++ b/src/Clerk/BackendAPI/Helpers/TokenVerificationException.cs @@ -1,92 +1,93 @@ using System; -namespace Clerk.BackendAPI.Helpers.Jwks; - -public class TokenVerificationException : Exception +namespace Clerk.BackendAPI.Helpers.Jwks { - public readonly ErrorReason Reason; - - public TokenVerificationException(ErrorReason reason) : base(reason.Message) + public class TokenVerificationException : Exception { - Reason = reason; - } + public readonly ErrorReason Reason; - public TokenVerificationException(ErrorReason reason, Exception cause) : base(reason.Message, cause) - { - Reason = reason; + public TokenVerificationException(ErrorReason reason) : base(reason.Message) + { + Reason = reason; + } + + public TokenVerificationException(ErrorReason reason, Exception cause) : base(reason.Message, cause) + { + Reason = reason; + } + + public override string ToString() + { + return Reason.Message; + } } - public override string ToString() + public static class TokenVerificationErrorReason { - return Reason.Message; + public static readonly ErrorReason + JWK_FAILED_TO_LOAD = new ErrorReason( + "jwk-failed-to-load", + "Failed to load JWKS from Clerk Backend API. Contact support@clerk.com." + ), + JWK_REMOTE_INVALID = new ErrorReason( + "jwk-remote-invalid", + "The JWKS endpoint did not contain any signing keys. Contact support@clerk.com." + ), + JWK_LOCAL_INVALID = new ErrorReason( + "jwk-local-invalid", + "The provided PEM Public Key is not in the proper format." + ), + JWK_FAILED_TO_RESOLVE = new ErrorReason( + "jwk-failed-to-resolve", + "Failed to resolve JWK. Public Key is not in the proper format." + ), + JWK_KID_MISMATCH = new ErrorReason( + "jwk-kid-mismatch", + "Unable to find a signing key in JWKS that matches the kid of the provided session token." + ), + TOKEN_EXPIRED = new ErrorReason( + "token-expired", + "Token has expired and is no longer valid." + ), + TOKEN_INVALID = new ErrorReason( + "token-invalid", + "Token is invalid and could not be verified." + ), + TOKEN_INVALID_AUTHORIZED_PARTIES = new ErrorReason( + "token-invalid-authorized-parties", + "Authorized party claim (azp) does not match any of the authorized parties." + ), + TOKEN_INVALID_AUDIENCE = new ErrorReason( + "token-invalid-audience", + "Token audience claim (aud) does not match one of the expected audience values." + ), + TOKEN_IAT_IN_THE_FUTURE = new ErrorReason( + "token-iat-in-the-future", + "Token Issued At claim (iat) represents a time in the future." + ), + TOKEN_NOT_ACTIVE_YET = new ErrorReason( + "token-not-active-yet", + "Token is not yet valid. Not Before claim (nbf) is in the future." + ), + TOKEN_INVALID_SIGNATURE = new ErrorReason( + "token-invalid-signature", + "Token signature is invalid and could not be verified." + ), + SECRET_KEY_MISSING = new ErrorReason( + "secret-key-missing", + "Missing Clerk Secret Key. Go to https://dashboard.clerk.com and get your key for your instance." + ), + TOKEN_TYPE_NOT_SUPPORTED = new ErrorReason( + "token-type-not-supported", + "The provided token type is not supported." + ), + INVALID_TOKEN_TYPE = new ErrorReason( + "invalid-token-type", + "The provided token is not a valid Clerk token type. Expected one of: session, machine, oauth, or api key." + ), + SERVER_ERROR = new ErrorReason( + "server-error", + "An unexpected error occurred while verifying the token. Please try again later." + ); } -} - -public static class TokenVerificationErrorReason -{ - public static readonly ErrorReason - JWK_FAILED_TO_LOAD = new( - "jwk-failed-to-load", - "Failed to load JWKS from Clerk Backend API. Contact support@clerk.com." - ), - JWK_REMOTE_INVALID = new( - "jwk-remote-invalid", - "The JWKS endpoint did not contain any signing keys. Contact support@clerk.com." - ), - JWK_LOCAL_INVALID = new( - "jwk-local-invalid", - "The provided PEM Public Key is not in the proper format." - ), - JWK_FAILED_TO_RESOLVE = new( - "jwk-failed-to-resolve", - "Failed to resolve JWK. Public Key is not in the proper format." - ), - JWK_KID_MISMATCH = new( - "jwk-kid-mismatch", - "Unable to find a signing key in JWKS that matches the kid of the provided session token." - ), - TOKEN_EXPIRED = new( - "token-expired", - "Token has expired and is no longer valid." - ), - TOKEN_INVALID = new( - "token-invalid", - "Token is invalid and could not be verified." - ), - TOKEN_INVALID_AUTHORIZED_PARTIES = new( - "token-invalid-authorized-parties", - "Authorized party claim (azp) does not match any of the authorized parties." - ), - TOKEN_INVALID_AUDIENCE = new( - "token-invalid-audience", - "Token audience claim (aud) does not match one of the expected audience values." - ), - TOKEN_IAT_IN_THE_FUTURE = new( - "token-iat-in-the-future", - "Token Issued At claim (iat) represents a time in the future." - ), - TOKEN_NOT_ACTIVE_YET = new( - "token-not-active-yet", - "Token is not yet valid. Not Before claim (nbf) is in the future." - ), - TOKEN_INVALID_SIGNATURE = new( - "token-invalid-signature", - "Token signature is invalid and could not be verified." - ), - SECRET_KEY_MISSING = new( - "secret-key-missing", - "Missing Clerk Secret Key. Go to https://dashboard.clerk.com and get your key for your instance." - ), - TOKEN_TYPE_NOT_SUPPORTED = new( - "token-type-not-supported", - "The provided token type is not supported." - ), - INVALID_TOKEN_TYPE = new( - "invalid-token-type", - "The provided token is not a valid Clerk token type. Expected one of: session, machine, oauth, or api key." - ), - SERVER_ERROR = new( - "server-error", - "An unexpected error occurred while verifying the token. Please try again later." - ); } \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Helpers/VerifyToken.cs b/src/Clerk/BackendAPI/Helpers/VerifyToken.cs index 4eea625..339dcaa 100644 --- a/src/Clerk/BackendAPI/Helpers/VerifyToken.cs +++ b/src/Clerk/BackendAPI/Helpers/VerifyToken.cs @@ -15,304 +15,312 @@ using WellKnownJWKS = Clerk.BackendAPI.Models.Components.Jwks; -namespace Clerk.BackendAPI.Helpers.Jwks; - -public static class VerifyToken +namespace Clerk.BackendAPI.Helpers.Jwks { - public static async Task VerifyTokenAsync(string token, VerifyTokenOptions options) + public static class VerifyToken { - var tokenType = TokenTypeHelper.GetTokenType(token); - - if (tokenType == TokenType.SessionToken) + public static async Task VerifyTokenAsync(string token, VerifyTokenOptions options) { - return await VerifySessionTokenAsync(token, options); - } - else if (TokenTypeHelper.IsMachineToken(token)) - { - return await VerifyMachineTokenAsync(token, options, tokenType); + var tokenType = TokenTypeHelper.GetTokenType(token); + + if (tokenType == TokenType.SessionToken) + { + return await VerifySessionTokenAsync(token, options); + } + else if (TokenTypeHelper.IsMachineToken(token)) + { + return await VerifyMachineTokenAsync(token, options, tokenType); + } + else + { + throw new TokenVerificationException(TokenVerificationErrorReason.INVALID_TOKEN_TYPE); + } } - else + + private static async Task VerifySessionTokenAsync(string token, VerifyTokenOptions options) { - throw new TokenVerificationException(TokenVerificationErrorReason.INVALID_TOKEN_TYPE); - } - } + RsaSecurityKey rsaKey; + if (options.JwtKey != null) + rsaKey = GetLocalJwtKey(options.JwtKey); + else + rsaKey = await GetRemoteJwtKeyAsync(token, options); - private static async Task VerifySessionTokenAsync(string token, VerifyTokenOptions options) - { - RsaSecurityKey rsaKey; - if (options.JwtKey != null) - rsaKey = GetLocalJwtKey(options.JwtKey); - else - rsaKey = await GetRemoteJwtKeyAsync(token, options); + var tokenHandler = new JwtSecurityTokenHandler(); - var tokenHandler = new JwtSecurityTokenHandler(); + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = options.Audiences != null, + ValidAudiences = options.Audiences, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = rsaKey, + ClockSkew = TimeSpan.FromMilliseconds(options.ClockSkewInMs) + }; + + ClaimsPrincipal claims; + try + { + claims = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); + } + catch (SecurityTokenExpiredException ex) + { + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_EXPIRED, ex); + } + catch (SecurityTokenNotYetValidException ex) + { + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_NOT_ACTIVE_YET, ex); + } + catch (SecurityTokenInvalidSignatureException ex) + { + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID_SIGNATURE, ex); + } + catch (SecurityTokenInvalidAudienceException ex) + { + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID_AUDIENCE, ex); + } + catch (Exception ex) + { + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID, ex); + } - var validationParameters = new TokenValidationParameters - { - ValidateIssuer = false, - ValidateAudience = options.Audiences != null, - ValidAudiences = options.Audiences, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - IssuerSigningKey = rsaKey, - ClockSkew = TimeSpan.FromMilliseconds(options.ClockSkewInMs) - }; - - ClaimsPrincipal claims; - try - { - claims = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); - } - catch (SecurityTokenExpiredException ex) - { - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_EXPIRED, ex); - } - catch (SecurityTokenNotYetValidException ex) - { - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_NOT_ACTIVE_YET, ex); - } - catch (SecurityTokenInvalidSignatureException ex) - { - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID_SIGNATURE, ex); - } - catch (SecurityTokenInvalidAudienceException ex) - { - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID_AUDIENCE, ex); + if (options.AuthorizedParties != null) + { + var azpClaim = claims.FindFirst("azp"); + if (azpClaim != null && !options.AuthorizedParties.Contains(azpClaim.Value)) + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID_AUTHORIZED_PARTIES); + } + + var iatClaim = claims.FindFirst("iat"); + if (iatClaim != null && long.Parse(iatClaim.Value) > + DateTimeOffset.UtcNow.ToUnixTimeSeconds() + options.ClockSkewInMs / 1000) + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_IAT_IN_THE_FUTURE); + + claims = OrganizationClaimsProcessor.ProcessOrganizationClaims(claims); + return claims; } - catch (Exception ex) + + /// + /// Converts a RSA PEM formatted public key to a RsaSecurityKey object + /// that can be used for networkless verification. + /// + /// The PEM formatted public key. + /// The RSA public key + /// if the public key could not be resolved. + private static RsaSecurityKey GetLocalJwtKey(string jwtKey) { - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID, ex); + try + { + var rsa = RSA.Create(); + // Manually parse the PEM key since ImportFromPem is unavailable in .NET Standard 2.1 + var pem = jwtKey.Replace("-----BEGIN PUBLIC KEY-----", string.Empty) + .Replace("-----END PUBLIC KEY-----", string.Empty) + .Replace("\n", string.Empty) + .Replace("\r", string.Empty); + var keyBytes = Convert.FromBase64String(pem); + + rsa.ImportSubjectPublicKeyInfo(keyBytes, out _); + return new RsaSecurityKey(rsa); + } + catch (Exception ex) + { + throw new TokenVerificationException(TokenVerificationErrorReason.JWK_LOCAL_INVALID, ex); + } } - if (options.AuthorizedParties != null) + /// + /// Retrieves the RSA public key used to sign the token from Clerk's Backend API. + /// + /// The token to parse. + /// The options used for token verification. + /// The RSA public key. + /// if the public key could not be resolved. + private static async Task GetRemoteJwtKeyAsync(string token, VerifyTokenOptions options) { - var azpClaim = claims.FindFirst("azp"); - if (azpClaim != null && !options.AuthorizedParties.Contains(azpClaim.Value)) - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID_AUTHORIZED_PARTIES); - } + var kid = ParseKid(token); - var iatClaim = claims.FindFirst("iat"); - if (iatClaim != null && long.Parse(iatClaim.Value) > - DateTimeOffset.UtcNow.ToUnixTimeSeconds() + options.ClockSkewInMs / 1000) - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_IAT_IN_THE_FUTURE); + var jwks = await FetchJwksAsync(options); + if (jwks.Keys == null) throw new TokenVerificationException(TokenVerificationErrorReason.JWK_REMOTE_INVALID); - claims = OrganizationClaimsProcessor.ProcessOrganizationClaims(claims); - return claims; - } + foreach (var key in jwks.Keys) + if (key.Kid == kid) + { + if ((key.N == null) | (key.E == null)) + throw new TokenVerificationException(TokenVerificationErrorReason.JWK_REMOTE_INVALID); + try + { + var rsaParameters = new RSAParameters + { + Modulus = Base64UrlDecode(key.N!), + Exponent = Base64UrlDecode(key.E!) + }; + var rsa = RSA.Create(); + rsa.ImportParameters(rsaParameters); + + return new RsaSecurityKey(rsa); + } + catch (Exception ex) + { + throw new TokenVerificationException(TokenVerificationErrorReason.JWK_FAILED_TO_RESOLVE, ex); + } + } - /// - /// Converts a RSA PEM formatted public key to a RsaSecurityKey object - /// that can be used for networkless verification. - /// - /// The PEM formatted public key. - /// The RSA public key - /// if the public key could not be resolved. - private static RsaSecurityKey GetLocalJwtKey(string jwtKey) - { - try - { - var rsa = RSA.Create(); - rsa.ImportFromPem(jwtKey.ToCharArray()); - return new RsaSecurityKey(rsa); + throw new TokenVerificationException(TokenVerificationErrorReason.JWK_KID_MISMATCH); } - catch (Exception ex) + + + /// + /// Decodes a base64url encoded string. + /// + /// The base64url encoded string. + /// The decoded byte array. + private static byte[] Base64UrlDecode(string input) { - throw new TokenVerificationException(TokenVerificationErrorReason.JWK_LOCAL_INVALID, ex); - } - } + var base64 = input.Replace('-', '+').Replace('_', '/'); + switch (base64.Length % 4) + { + case 2: base64 += "=="; break; + case 3: base64 += "="; break; + } - /// - /// Retrieves the RSA public key used to sign the token from Clerk's Backend API. - /// - /// The token to parse. - /// The options used for token verification. - /// The RSA public key. - /// if the public key could not be resolved. - private static async Task GetRemoteJwtKeyAsync(string token, VerifyTokenOptions options) - { - var kid = ParseKid(token); + return Convert.FromBase64String(base64); + } - var jwks = await FetchJwksAsync(options); - if (jwks.Keys == null) throw new TokenVerificationException(TokenVerificationErrorReason.JWK_REMOTE_INVALID); + /// + /// Retrieves the key identifier (kid) from the token header. + /// + /// The token to parse. + /// The key identifier (kid). + /// if the kid cannot be parsed. + private static string ParseKid(string token) + { + var handler = new JwtSecurityTokenHandler(); - foreach (var key in jwks.Keys) - if (key.Kid == kid) - { - if ((key.N == null) | (key.E == null)) - throw new TokenVerificationException(TokenVerificationErrorReason.JWK_REMOTE_INVALID); + if (handler.CanReadToken(token)) try { - var rsaParameters = new RSAParameters - { - Modulus = Base64UrlDecode(key.N!), - Exponent = Base64UrlDecode(key.E!) - }; - var rsa = RSA.Create(); - rsa.ImportParameters(rsaParameters); + var jwtToken = handler.ReadJwtToken(token); - return new RsaSecurityKey(rsa); + if (jwtToken.Header.TryGetValue("kid", out var kid)) + if (kid != null) + return (string)kid; } catch (Exception ex) { - throw new TokenVerificationException(TokenVerificationErrorReason.JWK_FAILED_TO_RESOLVE, ex); + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID, ex); } - } - - throw new TokenVerificationException(TokenVerificationErrorReason.JWK_KID_MISMATCH); - } + throw new TokenVerificationException(TokenVerificationErrorReason.JWK_KID_MISMATCH); + } - /// - /// Decodes a base64url encoded string. - /// - /// The base64url encoded string. - /// The decoded byte array. - private static byte[] Base64UrlDecode(string input) - { - var base64 = input.Replace('-', '+').Replace('_', '/'); - switch (base64.Length % 4) + /// + /// Fetches the JSON Web Key Set (JWKS) from Clerk's Backend API. + /// + /// The options used for token verification. + /// The JWKS keys array as a JSON node. + /// if the JWKS cannot be fetched. + private static async Task FetchJwksAsync(VerifyTokenOptions options) { - case 2: base64 += "=="; break; - case 3: base64 += "="; break; - } + if (options.SecretKey == null) + throw new TokenVerificationException(TokenVerificationErrorReason.SECRET_KEY_MISSING); - return Convert.FromBase64String(base64); - } + using (var client = new HttpClient()) + { + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.SecretKey); + var jwksUrl = $"{options.ApiUrl}/{options.ApiVersion}/jwks"; - /// - /// Retrieves the key identifier (kid) from the token header. - /// - /// The token to parse. - /// The key identifier (kid). - /// if the kid cannot be parsed. - private static string ParseKid(string token) - { - var handler = new JwtSecurityTokenHandler(); - if (handler.CanReadToken(token)) - try - { - var jwtToken = handler.ReadJwtToken(token); + var httpResponse = await client.GetAsync(jwksUrl); + if (!httpResponse.IsSuccessStatusCode) + throw new TokenVerificationException(TokenVerificationErrorReason.JWK_FAILED_TO_LOAD); - if (jwtToken.Header.TryGetValue("kid", out var kid)) - if (kid != null) - return (string)kid; - } - catch (Exception ex) - { - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID, ex); - } + var responseBody = await httpResponse.Content.ReadAsStringAsync(); - throw new TokenVerificationException(TokenVerificationErrorReason.JWK_KID_MISMATCH); - } + WellKnownJWKS? wellKnownJWKS; + try + { + wellKnownJWKS = ResponseBodyDeserializer.Deserialize(responseBody); + } + catch (JsonReaderException ex) + { + throw new TokenVerificationException(TokenVerificationErrorReason.JWK_FAILED_TO_LOAD, ex); + } - /// - /// Fetches the JSON Web Key Set (JWKS) from Clerk's Backend API. - /// - /// The options used for token verification. - /// The JWKS keys array as a JSON node. - /// if the JWKS cannot be fetched. - private static async Task FetchJwksAsync(VerifyTokenOptions options) - { - if (options.SecretKey == null) - throw new TokenVerificationException(TokenVerificationErrorReason.SECRET_KEY_MISSING); + if (wellKnownJWKS == null) + throw new TokenVerificationException(TokenVerificationErrorReason.JWK_REMOTE_INVALID); + + return wellKnownJWKS!; + } + } - using (var client = new HttpClient()) + /// + /// Verifies machine tokens (M2M, OAuth, API keys) by making API calls to Clerk's backend + /// + /// The machine token to verify + /// Verification options + /// The type of machine token + /// ClaimsPrincipal containing token information + private static async Task VerifyMachineTokenAsync(string token, VerifyTokenOptions options, TokenType tokenType) { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", options.SecretKey); - var jwksUrl = $"{options.ApiUrl}/{options.ApiVersion}/jwks"; + if (options.SecretKey == null && options.MachineSecretKey == null) + throw new TokenVerificationException(TokenVerificationErrorReason.SECRET_KEY_MISSING); + var endpoint = TokenTypeHelper.GetVerificationEndpoint(tokenType); + var verificationUrl = $"{options.ApiUrl}/{options.ApiVersion}{endpoint}"; - var httpResponse = await client.GetAsync(jwksUrl); - if (!httpResponse.IsSuccessStatusCode) - throw new TokenVerificationException(TokenVerificationErrorReason.JWK_FAILED_TO_LOAD); + using var client = new HttpClient(); - var responseBody = await httpResponse.Content.ReadAsStringAsync(); + var authToken = options.SecretKey ?? options.MachineSecretKey; + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + var payload = new { secret = token }; + var jsonContent = JsonConvert.SerializeObject(payload); + var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - WellKnownJWKS? wellKnownJWKS; try { - wellKnownJWKS = ResponseBodyDeserializer.Deserialize(responseBody); - } - catch (JsonReaderException ex) - { - throw new TokenVerificationException(TokenVerificationErrorReason.JWK_FAILED_TO_LOAD, ex); - } - - if (wellKnownJWKS == null) - throw new TokenVerificationException(TokenVerificationErrorReason.JWK_REMOTE_INVALID); + var response = await client.PostAsync(verificationUrl, content); - return wellKnownJWKS!; - } - } - - /// - /// Verifies machine tokens (M2M, OAuth, API keys) by making API calls to Clerk's backend - /// - /// The machine token to verify - /// Verification options - /// The type of machine token - /// ClaimsPrincipal containing token information - private static async Task VerifyMachineTokenAsync(string token, VerifyTokenOptions options, TokenType tokenType) - { - if (options.SecretKey == null && options.MachineSecretKey == null) - throw new TokenVerificationException(TokenVerificationErrorReason.SECRET_KEY_MISSING); - - var endpoint = TokenTypeHelper.GetVerificationEndpoint(tokenType); - var verificationUrl = $"{options.ApiUrl}/{options.ApiVersion}{endpoint}"; + if (!response.IsSuccessStatusCode) + { + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID); + } - using var client = new HttpClient(); + var responseBody = await response.Content.ReadAsStringAsync(); + var tokenData = JsonConvert.DeserializeObject>(responseBody); - var authToken = options.SecretKey ?? options.MachineSecretKey; - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + if (tokenData == null) + { + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID); + } - var payload = new { secret = token }; - var jsonContent = JsonConvert.SerializeObject(payload); - var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + // Create claims from the token verification response + var claims = new List(); - try - { - var response = await client.PostAsync(verificationUrl, content); + foreach (var kvp in tokenData) + { + if (kvp.Value != null) + { + claims.Add(new Claim(kvp.Key, kvp.Value.ToString()!)); + } + } - if (!response.IsSuccessStatusCode) + var identity = new ClaimsIdentity(claims, "ClerkMachineToken"); + return new ClaimsPrincipal(identity); + } + catch (HttpRequestException ex) { - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID); + throw new TokenVerificationException(TokenVerificationErrorReason.SERVER_ERROR, ex); } - - var responseBody = await response.Content.ReadAsStringAsync(); - var tokenData = JsonConvert.DeserializeObject>(responseBody); - - if (tokenData == null) + catch (TaskCanceledException ex) { - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID); + throw new TokenVerificationException(TokenVerificationErrorReason.SERVER_ERROR, ex); } - - // Create claims from the token verification response - var claims = new List(); - - foreach (var kvp in tokenData) + catch (JsonException ex) { - if (kvp.Value != null) - { - claims.Add(new Claim(kvp.Key, kvp.Value.ToString()!)); - } + throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID, ex); } - - var identity = new ClaimsIdentity(claims, "ClerkMachineToken"); - return new ClaimsPrincipal(identity); - } - catch (HttpRequestException ex) - { - throw new TokenVerificationException(TokenVerificationErrorReason.SERVER_ERROR, ex); - } - catch (TaskCanceledException ex) - { - throw new TokenVerificationException(TokenVerificationErrorReason.SERVER_ERROR, ex); - } - catch (JsonException ex) - { - throw new TokenVerificationException(TokenVerificationErrorReason.TOKEN_INVALID, ex); } } } \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Helpers/VerifyTokenOptions.cs b/src/Clerk/BackendAPI/Helpers/VerifyTokenOptions.cs index 9e5ed6e..46f9fa9 100644 --- a/src/Clerk/BackendAPI/Helpers/VerifyTokenOptions.cs +++ b/src/Clerk/BackendAPI/Helpers/VerifyTokenOptions.cs @@ -1,56 +1,57 @@ using System.Collections.Generic; -namespace Clerk.BackendAPI.Helpers.Jwks; - -public sealed class VerifyTokenOptions +namespace Clerk.BackendAPI.Helpers.Jwks { - private static readonly long DEFAULT_CLOCK_SKEW_MS = 5000L; - private static readonly string DEFAULT_API_URL = "https://api.clerk.com"; - private static readonly string DEFAULT_API_VERSION = "v1"; - public readonly string ApiUrl; - public readonly string ApiVersion; - public readonly IEnumerable? Audiences; - public readonly IEnumerable? AuthorizedParties; - public readonly long ClockSkewInMs; - public readonly string? JwtKey; + public sealed class VerifyTokenOptions + { + private static readonly long DEFAULT_CLOCK_SKEW_MS = 5000L; + private static readonly string DEFAULT_API_URL = "https://api.clerk.com"; + private static readonly string DEFAULT_API_VERSION = "v1"; + public readonly string ApiUrl; + public readonly string ApiVersion; + public readonly IEnumerable? Audiences; + public readonly IEnumerable? AuthorizedParties; + public readonly long ClockSkewInMs; + public readonly string? JwtKey; - public readonly string? SecretKey; - public readonly string? MachineSecretKey; + public readonly string? SecretKey; + public readonly string? MachineSecretKey; - /// - /// Options to configure VerifyTokenAsync. - /// - /// The Clerk secret key from the API Keys page in the Clerk Dashboard. (Optional) - /// The Machine secret key for machine-specific authentication. (Optional) - /// PEM Public String used to verify the session token in a networkless manner. (Optional) - /// A list of audiences to verify against. - /// An allowlist of origins to verify against. - /// - /// Allowed time difference (in milliseconds) between the Clerk server (which generates the - /// token) and the clock of the user's application server when validating a token. Defaults to 5000 ms. - /// - /// The Clerk Backend API endpoint. Defaults to 'https://api.clerk.com' - /// The version passed to the Clerk API. Defaults to 'v1' - public VerifyTokenOptions( - string? secretKey = null, - string? machineSecretKey = null, - string? jwtKey = null, - IEnumerable? audiences = null, - IEnumerable? authorizedParties = null, - long? clockSkewInMs = null, - string? apiUrl = null, - string? apiVersion = null) - { - if (string.IsNullOrEmpty(secretKey) && string.IsNullOrEmpty(jwtKey) && string.IsNullOrEmpty(machineSecretKey)) - throw new TokenVerificationException(TokenVerificationErrorReason.SECRET_KEY_MISSING); + /// + /// Options to configure VerifyTokenAsync. + /// + /// The Clerk secret key from the API Keys page in the Clerk Dashboard. (Optional) + /// The Machine secret key for machine-specific authentication. (Optional) + /// PEM Public String used to verify the session token in a networkless manner. (Optional) + /// A list of audiences to verify against. + /// An allowlist of origins to verify against. + /// + /// Allowed time difference (in milliseconds) between the Clerk server (which generates the + /// token) and the clock of the user's application server when validating a token. Defaults to 5000 ms. + /// + /// The Clerk Backend API endpoint. Defaults to 'https://api.clerk.com' + /// The version passed to the Clerk API. Defaults to 'v1' + public VerifyTokenOptions( + string? secretKey = null, + string? machineSecretKey = null, + string? jwtKey = null, + IEnumerable? audiences = null, + IEnumerable? authorizedParties = null, + long? clockSkewInMs = null, + string? apiUrl = null, + string? apiVersion = null) + { + if (string.IsNullOrEmpty(secretKey) && string.IsNullOrEmpty(jwtKey) && string.IsNullOrEmpty(machineSecretKey)) + throw new TokenVerificationException(TokenVerificationErrorReason.SECRET_KEY_MISSING); - SecretKey = secretKey; - MachineSecretKey = machineSecretKey; - JwtKey = jwtKey; - Audiences = audiences; - AuthorizedParties = authorizedParties; - ClockSkewInMs = clockSkewInMs ?? DEFAULT_CLOCK_SKEW_MS; - ApiUrl = apiUrl ?? DEFAULT_API_URL; - ApiVersion = apiVersion ?? DEFAULT_API_VERSION; + SecretKey = secretKey; + MachineSecretKey = machineSecretKey; + JwtKey = jwtKey; + Audiences = audiences; + AuthorizedParties = authorizedParties; + ClockSkewInMs = clockSkewInMs ?? DEFAULT_CLOCK_SKEW_MS; + ApiUrl = apiUrl ?? DEFAULT_API_URL; + ApiVersion = apiVersion ?? DEFAULT_API_VERSION; + } } } \ No newline at end of file diff --git a/src/Clerk/BackendAPI/Utils/ResponseBodyDeserializer.cs b/src/Clerk/BackendAPI/Utils/ResponseBodyDeserializer.cs index 5b90049..4f0c6a5 100644 --- a/src/Clerk/BackendAPI/Utils/ResponseBodyDeserializer.cs +++ b/src/Clerk/BackendAPI/Utils/ResponseBodyDeserializer.cs @@ -23,7 +23,7 @@ namespace Clerk.BackendAPI.Utils internal class ResponseBodyDeserializer { - public static T? Deserialize(string json, NullValueHandling nullValueHandling=NullValueHandling.Ignore, MissingMemberHandling missingMemberHandling=MissingMemberHandling.Ignore) + public static T Deserialize(string json, NullValueHandling nullValueHandling=NullValueHandling.Ignore, MissingMemberHandling missingMemberHandling=MissingMemberHandling.Ignore) { return JsonConvert.DeserializeObject(json, new JsonSerializerSettings(){ NullValueHandling = nullValueHandling, MissingMemberHandling = missingMemberHandling, Converters = Utilities.GetJsonDeserializers(typeof(T))}); } @@ -48,7 +48,7 @@ public sealed class DeserializationException : Exception public DeserializationException(Type type) : base($"Could not deserialize into {type} type.") { } } - public static T? DeserializeUndiscriminatedUnionMember(string json) + public static T DeserializeUndiscriminatedUnionMember(string json) { try { diff --git a/src/Clerk/BackendAPI/Utils/SpeakeasyHttpClient.cs b/src/Clerk/BackendAPI/Utils/SpeakeasyHttpClient.cs index 7633457..4c31ca6 100644 --- a/src/Clerk/BackendAPI/Utils/SpeakeasyHttpClient.cs +++ b/src/Clerk/BackendAPI/Utils/SpeakeasyHttpClient.cs @@ -16,25 +16,25 @@ namespace Clerk.BackendAPI.Utils public interface ISpeakeasyHttpClient { - /// - /// Sends an HTTP request asynchronously. - /// - /// - /// When overriding this method, use HttpCompletionOption.ResponseHeadersRead to support streaming response bodies. - /// - /// The HTTP request message to send. - /// The value of the TResult parameter contains the HTTP response message. + /// + /// Sends an HTTP request asynchronously. + /// + /// + /// When overriding this method, use HttpCompletionOption.ResponseHeadersRead to support streaming response bodies. + /// + /// The HTTP request message to send. + /// The value of the TResult parameter contains the HTTP response message. Task SendAsync(HttpRequestMessage request); - /// - /// Clones an HTTP request asynchronously. - /// - /// - /// This method is used in the context of Retries. It creates a new HttpRequestMessage instance - /// as a deep copy of the original request, including its method, URI, content, headers and options. - /// - /// The HTTP request message to clone. - /// The value of the TResult parameter contains the cloned HTTP request message. + /// + /// Clones an HTTP request asynchronously. + /// + /// + /// This method is used in the context of Retries. It creates a new HttpRequestMessage instance + /// as a deep copy of the original request, including its method, URI, content, and headers. + /// + /// The HTTP request message to clone. + /// The value of the TResult parameter contains the cloned HTTP request message. Task CloneAsync(HttpRequestMessage request); } @@ -73,10 +73,7 @@ public virtual async Task CloneAsync(HttpRequestMessage requ clone.Headers.TryAddWithoutValidation(header.Key, header.Value); } - foreach (KeyValuePair prop in request.Options) - { - clone.Options.TryAdd(prop.Key, prop.Value); - } + // Removed the problematic 'Options' property as it is not available in HttpRequestMessage in .NET Standard 2.1. return clone; } diff --git a/src/Clerk/BackendAPI/Utils/Utilities.cs b/src/Clerk/BackendAPI/Utils/Utilities.cs index 3496e24..7221cf3 100644 --- a/src/Clerk/BackendAPI/Utils/Utilities.cs +++ b/src/Clerk/BackendAPI/Utils/Utilities.cs @@ -168,9 +168,9 @@ private static string StripSurroundingQuotes(string input) { Regex surroundingQuotesRegex = new Regex("^\"(.*)\"$"); var match = surroundingQuotesRegex.Match(input); - if(match.Groups.Values.Count() == 2) + if (match.Groups.Count == 2) { - return match.Groups.Values.Last().ToString(); + return match.Groups[1].Value; } return input; } diff --git a/tests/JwksHelpers/VerifyTokenTests.cs b/tests/JwksHelpers/VerifyTokenTests.cs index 08b37e9..74dc915 100644 --- a/tests/JwksHelpers/VerifyTokenTests.cs +++ b/tests/JwksHelpers/VerifyTokenTests.cs @@ -60,8 +60,8 @@ public async Task TestVerifyTokenInvalidJwtKey() ); Assert.Equal(TokenVerificationErrorReason.JWK_LOCAL_INVALID, ex.Reason); - Assert.IsType(ex.InnerException); - Assert.Contains("No supported key formats were found.", ex.InnerException.Message); + Assert.IsType(ex.InnerException); + Assert.Contains("The input is not a valid Base-64 string", ex.InnerException.Message); } [Fact]