Skip to content

Commit 8cb83d9

Browse files
authored
Merge pull request #122 from speakeasy-sdks/ms/clerk-v2-jwt
Handle V2 JWT format
2 parents 00b1b4c + 4a10347 commit 8cb83d9

File tree

3 files changed

+296
-84
lines changed

3 files changed

+296
-84
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "clerk-backend-api"
33
version = "2.1.0"
44
description = "Python Client SDK for clerk.dev"
55
authors = [{ name = "Clerk" },]
6-
readme = "README-PYPI.md"
6+
readme = "README.md"
77
requires-python = ">=3.9"
88
dependencies = [
99
"cryptography (>=43.0.1,<44.0.0)",

src/clerk_backend_api/jwks_helpers/authenticaterequest.py

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,51 @@ class AuthenticateRequestOptions:
7979
clock_skew_in_ms: int = 5000
8080

8181
def authenticate_request(request: Requestish, options: AuthenticateRequestOptions) -> RequestState:
82+
83+
def __compute_org_permissions(claims: Dict[str, Any]) -> List[str]:
84+
features_str = claims.get("fea")
85+
if features_str is None:
86+
return []
87+
88+
org_claims = claims.get("o", {})
89+
permissions_str = org_claims.get("per")
90+
mappings_str = org_claims.get("fpm")
91+
92+
if not all(isinstance(s, str) for s in [permissions_str, mappings_str]):
93+
return []
94+
95+
features = features_str.split(",")
96+
permissions = permissions_str.split(",")
97+
mappings = mappings_str.split(",")
98+
99+
org_permissions = []
100+
101+
for idx in range(len(mappings)):
102+
if idx >= len(features):
103+
continue
104+
105+
mapping = mappings[idx]
106+
feature_parts = features[idx].split(":")
107+
if len(feature_parts) != 2:
108+
continue
109+
110+
scope, feature = feature_parts
111+
if "o" not in scope:
112+
continue
113+
114+
try:
115+
binary = bin(int(mapping))[2:].lstrip("0")
116+
except ValueError:
117+
continue
118+
119+
reversed_binary = binary[::-1]
120+
121+
for i, bit in enumerate(reversed_binary):
122+
if bit == "1" and i < len(permissions):
123+
org_permissions.append(f"org:{feature}:{permissions[i]}")
124+
125+
return org_permissions
126+
82127
""" Authenticates the session token. Networkless if the options.jwt_key is provided.
83128
Otherwise, performs a network call to retrieve the JWKS from Clerk's Backend API.
84129
"""
@@ -104,25 +149,53 @@ def get_session_token(request: Requestish) -> Optional[str]:
104149

105150

106151
session_token = get_session_token(request)
152+
107153
if session_token is None:
108154
return RequestState(status=AuthStatus.SIGNED_OUT, reason=AuthErrorReason.SESSION_TOKEN_MISSING)
109155

110-
if options.secret_key is None:
111-
return RequestState(status=AuthStatus.SIGNED_OUT, reason=AuthErrorReason.SECRET_KEY_MISSING)
112-
113156
try:
114-
payload = verify_token(
115-
session_token,
116-
VerifyTokenOptions(
117-
audience=options.audience,
118-
authorized_parties=options.authorized_parties,
119-
secret_key=options.secret_key,
120-
clock_skew_in_ms=options.clock_skew_in_ms,
121-
jwt_key=options.jwt_key,
122-
),
123-
)
157+
if options.secret_key:
158+
payload = verify_token(
159+
session_token,
160+
VerifyTokenOptions(
161+
audience=options.audience,
162+
authorized_parties=options.authorized_parties,
163+
secret_key=options.secret_key,
164+
clock_skew_in_ms=options.clock_skew_in_ms,
165+
jwt_key=None,
166+
),
167+
)
168+
elif options.jwt_key:
169+
payload = verify_token(
170+
session_token,
171+
VerifyTokenOptions(
172+
audience=options.audience,
173+
authorized_parties=options.authorized_parties,
174+
secret_key=None,
175+
clock_skew_in_ms=options.clock_skew_in_ms,
176+
jwt_key=options.jwt_key,
177+
),
178+
)
179+
else:
180+
return RequestState(status=AuthStatus.SIGNED_OUT, reason=AuthErrorReason.SECRET_KEY_MISSING)
181+
182+
if payload is not None and payload.get("v") == 2:
183+
org_claims = payload.get("o", {})
184+
if org_claims:
185+
payload["org_id"] = org_claims.get("id")
186+
payload["org_slug"] = org_claims.get("slg")
187+
payload["org_role"] = org_claims.get("rol")
188+
189+
org_permissions = __compute_org_permissions(payload)
190+
if org_permissions:
191+
payload["org_permissions"] = org_permissions
124192

125193
return RequestState(status=AuthStatus.SIGNED_IN, token=session_token, payload=payload)
126194

127195
except TokenVerificationError as e:
128196
return RequestState(status=AuthStatus.SIGNED_OUT, reason=e.reason)
197+
198+
199+
200+
201+

0 commit comments

Comments
 (0)