-
Notifications
You must be signed in to change notification settings - Fork 6
feat(jwt): add support for aih-issued token validation #659
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -184,14 +184,42 @@ const nodeSdk = ({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const token = res.payload; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (token) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| token.iss = token.iss?.split('/').pop(); // support both url and project id as issuer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (token.iss !== projectId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We must do the verification here, since issuer can be either project ID or URL | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new errors.JWTClaimValidationFailed( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'unexpected "iss" claim value', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'iss', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'check_failed', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extract project ID from issuer claim | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Supports: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 1. Direct project ID: "project-id" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 2. Standard URL format: "https://api.descope.com/v1/{projectId}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 3. AIH format: "https://api.descope.com/v1/apps/agentic/{projectId}/{resourceId}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const issuer = token.iss; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (issuer) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parts = issuer.split('/'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let extractedProjectId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parts.length === 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Case 1: Direct project ID | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractedProjectId = issuer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Cases 2 and 3: URL format | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if this is an AIH issuer (contains '/apps/agentic/') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const agenticIndex = parts.indexOf('agentic'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (agenticIndex !== -1 && agenticIndex < parts.length - 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // AIH format: project ID is right after 'agentic' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extractedProjectId = parts[agenticIndex + 1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+203
to
+206
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const agenticIndex = parts.indexOf('agentic'); | |
| if (agenticIndex !== -1 && agenticIndex < parts.length - 1) { | |
| // AIH format: project ID is right after 'agentic' | |
| extractedProjectId = parts[agenticIndex + 1]; | |
| const appsIndex = parts.indexOf('apps'); | |
| if ( | |
| appsIndex !== -1 && | |
| appsIndex + 2 < parts.length && | |
| parts[appsIndex + 1] === 'agentic' | |
| ) { | |
| // AIH format: project ID is right after '/apps/agentic/' | |
| extractedProjectId = parts[appsIndex + 2]; |
Copilot
AI
Feb 24, 2026
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.
Issuer validation is skipped when token.iss is missing/falsy because all checks are inside if (issuer). This is a behavior change from the previous logic (which rejected missing iss) and would allow JWTs without an iss claim to pass validateJwt as long as the signature verifies. Consider treating missing/empty iss as a validation failure (e.g., throw JWTClaimValidationFailed for iss) so issuer validation cannot be bypassed.
| if (issuer) { | |
| const parts = issuer.split('/'); | |
| let extractedProjectId: string; | |
| if (parts.length === 1) { | |
| // Case 1: Direct project ID | |
| extractedProjectId = issuer; | |
| } else { | |
| // Cases 2 and 3: URL format | |
| // Check if this is an AIH issuer (contains '/apps/agentic/') | |
| const agenticIndex = parts.indexOf('agentic'); | |
| if (agenticIndex !== -1 && agenticIndex < parts.length - 1) { | |
| // AIH format: project ID is right after 'agentic' | |
| extractedProjectId = parts[agenticIndex + 1]; | |
| } else { | |
| // Standard URL format: project ID is the last segment | |
| extractedProjectId = parts[parts.length - 1]; | |
| } | |
| } | |
| token.iss = extractedProjectId; | |
| if (token.iss !== projectId) { | |
| // We must do the verification here, since issuer can be either project ID or URL | |
| throw new errors.JWTClaimValidationFailed( | |
| 'unexpected "iss" claim value', | |
| 'iss', | |
| 'check_failed', | |
| ); | |
| } | |
| if (!issuer) { | |
| // Issuer is required for proper project validation | |
| throw new errors.JWTClaimValidationFailed( | |
| 'missing "iss" claim', | |
| 'iss', | |
| 'required', | |
| ); | |
| } | |
| const parts = issuer.split('/'); | |
| let extractedProjectId: string; | |
| if (parts.length === 1) { | |
| // Case 1: Direct project ID | |
| extractedProjectId = issuer; | |
| } else { | |
| // Cases 2 and 3: URL format | |
| // Check if this is an AIH issuer (contains '/apps/agentic/') | |
| const agenticIndex = parts.indexOf('agentic'); | |
| if (agenticIndex !== -1 && agenticIndex < parts.length - 1) { | |
| // AIH format: project ID is right after 'agentic' | |
| extractedProjectId = parts[agenticIndex + 1]; | |
| } else { | |
| // Standard URL format: project ID is the last segment | |
| extractedProjectId = parts[parts.length - 1]; | |
| } | |
| } | |
| token.iss = extractedProjectId; | |
| if (token.iss !== projectId) { | |
| // We must do the verification here, since issuer can be either project ID or URL | |
| throw new errors.JWTClaimValidationFailed( | |
| 'unexpected "iss" claim value', | |
| 'iss', | |
| 'check_failed', | |
| ); |
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.
Please add a regression test for the
iss-missing case (a JWT with noissclaim) to ensurevalidateJwtrejects it. The updated implementation currently has an explicitif (issuer)guard, so without a test it’s easy for issuer validation to accidentally become optional.