-
Notifications
You must be signed in to change notification settings - Fork 507
feat: add validateAccessJwt to cloudflare:workers #5837
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
The generated output of Full Type Diffdiff -r types/generated-snapshot/latest/index.d.ts bazel-bin/types/definitions/latest/index.d.ts
11602a11603,11643
> // Access JWT validation
> export type AccessJwtErrorCode =
> | "ERR_JWT_MISSING"
> | "ERR_JWT_MALFORMED"
> | "ERR_JWT_INVALID_SIGNATURE"
> | "ERR_JWT_EXPIRED"
> | "ERR_JWT_NOT_YET_VALID"
> | "ERR_JWT_AUDIENCE_MISMATCH"
> | "ERR_JWT_ISSUER_MISMATCH"
> | "ERR_JWKS_FETCH_FAILED"
> | "ERR_JWKS_NO_MATCHING_KEY"
> | "ERR_INVALID_TEAM_DOMAIN"
> | "ERR_INVALID_AUDIENCE";
> export class AccessJwtError extends Error {
> readonly code: AccessJwtErrorCode;
> constructor(code: AccessJwtErrorCode, message: string);
> }
> export interface AccessJwtPayload {
> aud: string[];
> email?: string;
> exp: number;
> iat: number;
> nbf?: number;
> iss: string;
> sub: string;
> [key: string]: unknown;
> }
> /**
> * Validates a Cloudflare Access JWT from an incoming request.
> *
> * @param req - The incoming Request containing the cf-access-jwt-assertion header
> * @param teamDomain - The Cloudflare One team domain (e.g., "mycompany" or "mycompany.cloudflareaccess.com")
> * @param audience - The Application Audience (AUD) tag
> * @throws {AccessJwtError} If validation fails for any reason
> * @returns The decoded JWT payload on success
> */
> export function validateAccessJwt(
> req: Request,
> teamDomain: string,
> audience: string,
> ): Promise<AccessJwtPayload>;
diff -r types/generated-snapshot/latest/index.ts bazel-bin/types/definitions/latest/index.ts
11572a11573,11613
> // Access JWT validation
> export type AccessJwtErrorCode =
> | "ERR_JWT_MISSING"
> | "ERR_JWT_MALFORMED"
> | "ERR_JWT_INVALID_SIGNATURE"
> | "ERR_JWT_EXPIRED"
> | "ERR_JWT_NOT_YET_VALID"
> | "ERR_JWT_AUDIENCE_MISMATCH"
> | "ERR_JWT_ISSUER_MISMATCH"
> | "ERR_JWKS_FETCH_FAILED"
> | "ERR_JWKS_NO_MATCHING_KEY"
> | "ERR_INVALID_TEAM_DOMAIN"
> | "ERR_INVALID_AUDIENCE";
> export class AccessJwtError extends Error {
> readonly code: AccessJwtErrorCode;
> constructor(code: AccessJwtErrorCode, message: string);
> }
> export interface AccessJwtPayload {
> aud: string[];
> email?: string;
> exp: number;
> iat: number;
> nbf?: number;
> iss: string;
> sub: string;
> [key: string]: unknown;
> }
> /**
> * Validates a Cloudflare Access JWT from an incoming request.
> *
> * @param req - The incoming Request containing the cf-access-jwt-assertion header
> * @param teamDomain - The Cloudflare One team domain (e.g., "mycompany" or "mycompany.cloudflareaccess.com")
> * @param audience - The Application Audience (AUD) tag
> * @throws {AccessJwtError} If validation fails for any reason
> * @returns The decoded JWT payload on success
> */
> export function validateAccessJwt(
> req: Request,
> teamDomain: string,
> audience: string,
> ): Promise<AccessJwtPayload>; |
55d2823 to
5ee0f15
Compare
|
The tests (after building...) fail locally — unclear (to me) why: |
5ee0f15 to
2dda59e
Compare
b678274 to
962eade
Compare
mhart
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor suggestions, just to use some slightly more modern/idiomatic methods, but looks great overall
962eade to
c343bd7
Compare
|
@mhart @anonrig I think I got this right — addressed all the comments:
|
Adds a new validateAccessJwt() function that validates Cloudflare Access JWTs against team-specific JWKs. The function throws AccessJwtError with specific error codes on validation failure, making error handling explicit. Key features: - No external dependencies (uses WebCrypto APIs) - Retry logic for JWKS fetch (3 attempts, 5s backoff on 5xx) - Isolate-level JWKS caching (1 hour TTL) - Team domain normalization (accepts both short and full forms) - 60s clock skew tolerance for expiration
a8cc773 to
5d87ec9
Compare
This comment was marked as outdated.
This comment was marked as outdated.
|
Except the failing tests, the implementation looks really good. |
| async function fetchJwks(normalizedDomain: string): Promise<JwkSet> { | ||
| const url = `https://${normalizedDomain}/cdn-cgi/access/certs`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: best way to do this is by doing:
const url = `https://cloudflare.com/cdn-cgi/access/certs`
url.hostname = normalizedDomain;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't that be const url = new URL('https://cloudflare.com/cdn-cgi/access/certs);` ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The domain has to include the team name: e.g. https://opencode-devtools.cloudflareaccess.com/cdn-cgi/access/certs - as the certs are scoped to the Access team/domain
|
@anonrig unfortunately getting the tests to run locally in any reliable way is beyond me, and I can't grok why the tests can't see the |
Adds a
validateAccessJwt()function to thecloudflare:workersmodule that validates Cloudflare Access JWTs against team-specific JWKs, providing a built-in alternative for users to validate Access JWTs directly vs. glueing it all together themselves.AccessJwtErrorwith specific error codes (ERR_JWT_MISSING,ERR_JWT_EXPIRED, etc.) on validation failure - defaults to throwing exceptions that you must catch vs. someisValidJWT()type function that can be mishandled"myteam"and"myteam.cloudflareaccess.com")Usage
Error Codes
These will be added to the public docs under https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/ + new Workers
/examplesas wellERR_JWT_MISSINGcf-access-jwt-assertionheaderERR_JWT_MALFORMEDERR_JWT_INVALID_SIGNATUREERR_JWT_EXPIREDexpclaim is in the pastERR_JWT_NOT_YET_VALIDnbfclaim is in the futureERR_JWT_AUDIENCE_MISMATCHauddoesn't match expected audienceERR_JWT_ISSUER_MISMATCHissdoesn't match team domainERR_JWKS_FETCH_FAILEDERR_JWKS_NO_MATCHING_KEYkidfoundERR_INVALID_TEAM_DOMAINERR_INVALID_AUDIENCETest Plan
just test '//src/cloudflare/internal/test/access-jwt:access-jwt-test@'