-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from speakeasy-sdks/feat/authenticate_request_h…
…elpers feat: implement AuthenticateRequest and VerifyToken JWKS helpers
- Loading branch information
Showing
16 changed files
with
1,189 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Helpers/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
using System.Linq; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Threading.Tasks; | ||
|
||
namespace Clerk.BackendAPI.Helpers.Jwks; | ||
|
||
/// <summary> | ||
/// AuthenticateRequest - Helper methods to authenticate requests. | ||
/// </summary> | ||
public static class AuthenticateRequest | ||
{ | ||
private const string SESSION_COOKIE_NAME = "__session"; | ||
|
||
/// <summary> | ||
/// 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. | ||
/// </summary> | ||
/// <param name="request">The HTTP request</param> | ||
/// <param name="options">The request authentication options</param> | ||
/// <returns>The request state</returns> | ||
/// <remarks>WARNING: AuthenticateRequestAsync is applicable in the context of Backend APIs only.</remarks> | ||
public static async Task<RequestState> AuthenticateRequestAsync( | ||
HttpRequestMessage request, | ||
AuthenticateRequestOptions options) | ||
{ | ||
var sessionToken = GetSessionToken(request); | ||
if (sessionToken == null) return RequestState.SignedOut(AuthErrorReason.SESSION_TOKEN_MISSING); | ||
|
||
VerifyTokenOptions verifyTokenOptions; | ||
|
||
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); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Retrieve token from __session cookie or Authorization header. | ||
/// </summary> | ||
/// <param name="request">The HTTP request</param> | ||
/// <returns>The session token, if present</returns> | ||
private static string? GetSessionToken(HttpRequestMessage request) | ||
{ | ||
if (request.Headers.TryGetValues("Authorization", out var authorizationHeaders)) | ||
{ | ||
var bearerToken = authorizationHeaders.FirstOrDefault(); | ||
if (!string.IsNullOrEmpty(bearerToken)) return bearerToken.Replace("Bearer ", ""); | ||
} | ||
|
||
if (request.Headers.TryGetValues("Cookie", out var cookieHeaders)) | ||
{ | ||
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 == SESSION_COOKIE_NAME) | ||
return cookie.Value; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
src/Clerk/BackendAPI/Helpers/AuthenticateRequestException.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
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." | ||
); | ||
} | ||
|
||
public class AuthenticateRequestException : Exception | ||
{ | ||
public readonly ErrorReason Reason; | ||
|
||
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; | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
src/Clerk/BackendAPI/Helpers/AuthenticateRequestOptions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
using System.Collections.Generic; | ||
|
||
namespace Clerk.BackendAPI.Helpers.Jwks; | ||
|
||
public sealed class AuthenticateRequestOptions | ||
{ | ||
private static readonly long DEFAULT_CLOCK_SKEW_MS = 5000L; | ||
public readonly IEnumerable<string>? Audiences; | ||
public readonly IEnumerable<string> AuthorizedParties; | ||
public readonly long ClockSkewInMs; | ||
public readonly string? JwtKey; | ||
|
||
public readonly string? SecretKey; | ||
|
||
/// <summary> | ||
/// Options to configure AuthenticateRequestAsync. | ||
/// </summary> | ||
/// <param name="secretKey">The Clerk secret key from the API Keys page in the Clerk Dashboard. (Optional)</param> | ||
/// <param name="jwtKey">PEM Public String used to verify the session token in a networkless manner. (Optional)</param> | ||
/// <param name="audiences">A list of audiences to verify against.</param> | ||
/// <param name="authorizedParties">An allowlist of origins to verify against.</param> | ||
/// <param name="clockSkewInMs"> | ||
/// 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. | ||
/// </param> | ||
public AuthenticateRequestOptions( | ||
string? secretKey = null, | ||
string? jwtKey = null, | ||
IEnumerable<string>? audiences = null, | ||
IEnumerable<string>? authorizedParties = null, | ||
long? clockSkewInMs = null) | ||
{ | ||
if (string.IsNullOrEmpty(secretKey) && string.IsNullOrEmpty(jwtKey)) | ||
throw new AuthenticateRequestException(AuthErrorReason.SECRET_KEY_MISSING); | ||
|
||
SecretKey = secretKey; | ||
JwtKey = jwtKey; | ||
Audiences = audiences; | ||
AuthorizedParties = authorizedParties ?? new List<string>(); | ||
ClockSkewInMs = clockSkewInMs ?? DEFAULT_CLOCK_SKEW_MS; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
namespace Clerk.BackendAPI.Helpers.Jwks; | ||
|
||
/// <summary> | ||
/// Represents the reason for a TokenVerificationException or AuthenticateRequestException. | ||
/// </summary> | ||
public class ErrorReason | ||
{ | ||
public readonly string Id; | ||
public readonly string Message; | ||
|
||
public ErrorReason(string id, string message) | ||
{ | ||
Id = id; | ||
Message = message; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
using System.Security.Claims; | ||
|
||
namespace Clerk.BackendAPI.Helpers.Jwks; | ||
|
||
/// <summary> | ||
/// AuthStatus - The request authentication status. | ||
/// </summary> | ||
public class AuthStatus | ||
{ | ||
public static readonly AuthStatus SignedIn = new("signed-in"); | ||
public static readonly AuthStatus SignedOut = new("signed-out"); | ||
|
||
private readonly string value; | ||
|
||
private AuthStatus(string value) | ||
{ | ||
this.value = value; | ||
} | ||
|
||
public string Value() | ||
{ | ||
return value; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// RequestState - Authentication State of the request. | ||
/// </summary> | ||
public class RequestState | ||
{ | ||
public readonly ClaimsPrincipal? Claims; | ||
public readonly ErrorReason? ErrorReason; | ||
public readonly AuthStatus Status; | ||
public readonly string? Token; | ||
|
||
|
||
public RequestState(AuthStatus status, | ||
ErrorReason? errorReason, | ||
string? token, | ||
ClaimsPrincipal? claims) | ||
{ | ||
Status = status; | ||
ErrorReason = errorReason; | ||
Token = token; | ||
Claims = claims; | ||
} | ||
|
||
public static RequestState SignedIn(string token, ClaimsPrincipal claims) | ||
{ | ||
return new RequestState(AuthStatus.SignedIn, null, token, claims); | ||
} | ||
|
||
public static RequestState SignedOut(ErrorReason errorReason) | ||
{ | ||
return new RequestState(AuthStatus.SignedOut, errorReason, null, null); | ||
} | ||
|
||
public bool IsSignedIn() | ||
{ | ||
return Status == AuthStatus.SignedIn; | ||
} | ||
|
||
public bool IsSignedOut() | ||
{ | ||
return Status == AuthStatus.SignedOut; | ||
} | ||
} |
80 changes: 80 additions & 0 deletions
80
src/Clerk/BackendAPI/Helpers/TokenVerificationException.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
using System; | ||
|
||
namespace Clerk.BackendAPI.Helpers.Jwks; | ||
|
||
public class TokenVerificationException : Exception | ||
{ | ||
public readonly ErrorReason 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 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 [email protected]." | ||
), | ||
JWK_REMOTE_INVALID = new( | ||
"jwk-remote-invalid", | ||
"The JWKS endpoint did not contain any signing keys. Contact [email protected]." | ||
), | ||
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." | ||
); | ||
} |
Oops, something went wrong.