feat(auth): add VerifyOptions / aud-claim verification parity with node-sdk#315
feat(auth): add VerifyOptions / aud-claim verification parity with node-sdk#315
Conversation
…session validation paths Mirrors the node-sdk's VerifyOptions type (audience). Adds backward-compatible overloads on AuthenticationService for validateSessionWithToken, refreshSessionWithToken(+AuthenticationInfo), validateAndRefreshSessionWithTokens(+AuthenticationInfo), and exchangeAccessKey. JwtUtils.getToken now accepts VerifyOptions and enforces 'aud' when provided. Includes unit tests for VerifyOptions and JwtUtils.verifyAudience.
…nService, and AuthenticationServiceImpl Adds new overloads to the public AuthenticationService interface and its impl that accept a VerifyOptions parameter. Existing call sites are preserved (all legacy methods now delegate to the new overloads with null options).
Extends the public AuthenticationService interface with VerifyOptions overloads for validateSessionWithToken, refreshSessionWithToken, refreshSessionWithTokenAuthenticationInfo, validateAndRefreshSessionWithTokens, validateAndRefreshSessionWithTokensAuthenticationInfo, and exchangeAccessKey. Existing signatures delegate to the new paths with null options, preserving full backward compatibility. Mirrors node-sdk's VerifyOptions parameter and unlocks aud-claim verification on every session-validation entry point.
- VerifyOptionsTest: builder + convenience helpers cover null/empty inputs, the single-audience shortcut, multiple audiences, and the getAudiencesOrEmpty() null-safe read. - JwtUtilsAudienceTest: exercises JwtUtils.verifyAudience in full isolation by mocking Claims.getAudience(), covering null options, empty expected list, single and multiple expected audiences, multiple token audiences, missing/null aud claims, and mismatch. - AuthenticationServiceVerifyOptionsTest: verifies that the new VerifyOptions overloads on AuthenticationService still enforce input validation (blank inputs rejected) identically to the legacy overloads. End-to-end JWT behavior is covered by JwtUtilsAudienceTest.
The maven-javadoc-plugin processes raw source (pre-delombok), so the
{@link VerifyOptions#getAudiences()} reference in JwtUtils' Javadoc
could not be resolved and failed the `attach-javadocs` goal with
"reference not found". Switch to a prose reference to VerifyOptions
so the link resolves without depending on a Lombok-synthesized method.
…eep intercepting
Thirteen existing service tests (Magic Link, OTP, OAuth, SAML, TOTP,
Password, WebAuthn, Enchanted Link, Jwt) stub JwtUtils.getToken via
MockedStatic on the 2-arg overload:
mockedJwtUtils.when(() -> JwtUtils.getToken(anyString(), any()))
.thenReturn(MOCK_TOKEN);
After the VerifyOptions refactor, the legacy public methods delegated
into the new 3-arg overload with `null` options, so the 2-arg stub
was bypassed, `JwtUtils.getToken(jwt, client, null)` was left
unstubbed, Mockito returned the default (null), and every
authentication-info-shaped test started failing with "Expecting
actual not to be null".
This commit restructures the delegation so:
- JwtUtils.getToken(String, Client) is the original method — builds
a Token directly, unchanged. This is what existing stubs target.
- JwtUtils.getToken(String, Client, VerifyOptions) short-circuits to
the 2-arg overload when options are null or carry no audiences;
the new audience-verifying path only runs when the caller actually
passes expected audiences.
- SdkServicesBase.validateAndCreateToken(String) stays at its
original body and is what the refactored internals call whenever
no VerifyOptions are involved. The new 2-arg overload delegates
to it for the null/empty case.
Result: all existing tests (420) should pass again, new tests
(VerifyOptionsTest, JwtUtilsAudienceTest,
AuthenticationServiceVerifyOptionsTest) continue to pass, and the
new audience-verification path is only exercised when a caller
supplies VerifyOptions.
There was a problem hiding this comment.
Pull request overview
Adds a VerifyOptions API to the Java SDK to optionally verify JWT aud claim across session/JWT validation entry points, aligning behavior with the Node SDK’s VerifyOptions semantics.
Changes:
- Introduces
VerifyOptionsmodel with helpers for configuring expected audiences. - Adds new overloads across
AuthenticationServiceand internal auth flows to passVerifyOptionsthrough JWT parsing/validation. - Implements and unit-tests audience verification logic in
JwtUtils.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/com/descope/model/auth/VerifyOptions.java | New options POJO (builder/helpers) for configuring expected JWT audiences. |
| src/main/java/com/descope/utils/JwtUtils.java | Adds getToken(..., VerifyOptions) overload and aud verification helper. |
| src/main/java/com/descope/sdk/SdkServicesBase.java | Adds validateAndCreateToken(..., VerifyOptions) overload to preserve legacy mocking path. |
| src/main/java/com/descope/sdk/auth/AuthenticationService.java | Adds public overloads accepting VerifyOptions for session/JWT flows and access-key exchange. |
| src/main/java/com/descope/sdk/auth/impl/AuthenticationsBase.java | Threads VerifyOptions through validation and refresh/auth-info construction. |
| src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceImpl.java | Implements new overloads and forwards to VerifyOptions-aware paths. |
| src/test/java/com/descope/model/auth/VerifyOptionsTest.java | Unit tests for VerifyOptions builder/helpers and null/empty handling. |
| src/test/java/com/descope/utils/JwtUtilsAudienceTest.java | Isolated unit tests for JwtUtils.verifyAudience behavior. |
| src/test/java/com/descope/sdk/auth/impl/AuthenticationServiceVerifyOptionsTest.java | Tests input validation parity for new overloads. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public static Token getToken(String jwt, Client client, VerifyOptions options) { | ||
| if (options == null || options.getAudiencesOrEmpty().isEmpty()) { | ||
| return getToken(jwt, client); | ||
| } | ||
|
|
||
| Jws<Claims> claimsJws = getClaimsJws(jwt, client); | ||
| Claims claims = claimsJws.getPayload(); | ||
|
|
||
| verifyAudience(claims, options); | ||
|
|
||
| return Token.builder() | ||
| .jwt(jwt) | ||
| .projectId(client.getProjectId()) | ||
| .id(claims.getSubject()) | ||
| .expiration(claims.getExpiration().getTime()) | ||
| .refreshExpiration(claims.get("rexp", Date.class)) | ||
| .claims(claims) | ||
| .build(); |
There was a problem hiding this comment.
JwtUtils.getToken(String, Client) and the new getToken(String, Client, VerifyOptions) duplicate the Token.builder() assembly logic. This increases the risk of the two overloads drifting (e.g., if new fields are added to Token later). Consider extracting the common Token construction into a private helper that both overloads call after obtaining Claims (while still keeping the 2-arg overload’s behavior intact for existing static mocks).
There was a problem hiding this comment.
@copilot apply changes based on this feedback
There was a problem hiding this comment.
Done in 69c1a76. Extracted a private buildToken(String jwt, String projectId, Claims claims) helper — both getToken overloads now call it, so the Token.builder() assembly logic lives in exactly one place.
…mpl.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…per in JwtUtils Agent-Logs-Url: https://github.com/descope/descope-java/sessions/b5a56654-76da-48a1-9dcc-0a31de599d6e Co-authored-by: chris4490 <42780533+chris4490@users.noreply.github.com>
| } | ||
| ExchangeTokenRequest request = new ExchangeTokenRequest(code); | ||
| ApiProxy apiProxy = getApiProxy(); | ||
| ApiProxy apiProxy = getApiProxy(); |
Summary
Adds
VerifyOptionsto the Java SDK so that every session-validation entry point can verify theaudclaim, reaching parity withnode-sdk'sVerifyOptions(which is accepted byvalidateJwt,validateSession,refreshSession,validateAndRefreshSession, andexchangeAccessKey).Before this PR, the Java SDK had no way to assert the incoming token's audience — the most commonly-requested multi-tenant / multi-service hardening check. After this PR, any caller can supply one or more expected audiences and the SDK will reject the token if none match.
API additions
All additions are backward-compatible overloads — every existing signature is preserved and delegates to the new path with
nulloptions.com.descope.model.auth.VerifyOptions(new Lombok@Data/@BuilderPOJO)audiences: List<String>VerifyOptions.withAudience(String)and.withAudiences(List<String>)static helpers.audience(String)for the common single-audience casegetAudiencesOrEmpty()null-safe accessorJwtUtils.getToken(String, Client, VerifyOptions)— parses, validates, then verifies audienceJwtUtils.verifyAudience(Claims, VerifyOptions)(package-private, unit-testable)SdkServicesBase.validateAndCreateToken(String, VerifyOptions)AuthenticationsBase.validateJWT(String, VerifyOptions)AuthenticationsBase.refreshSession(String, VerifyOptions)AuthenticationsBase.getAuthenticationInfo(JWTResponse, VerifyOptions)AuthenticationService:validateSessionWithToken(String, VerifyOptions)refreshSessionWithToken(String, VerifyOptions)refreshSessionWithTokenAuthenticationInfo(String, VerifyOptions)validateAndRefreshSessionWithTokens(String, String, VerifyOptions)validateAndRefreshSessionWithTokensAuthenticationInfo(String, String, VerifyOptions)exchangeAccessKey(String, AccessKeyLoginOptions, VerifyOptions)Behavior
verifyOptions == nullorverifyOptions.getAudiencesOrEmpty().isEmpty()→ no audience verification is performed (legacy behavior preserved).ClientFunctionalException.invalidToken(...)unless at least one of the expected audiences is present in the token'saudset (any-match semantics — matches node-sdk'sjose.jwtVerifybehavior with anaudienceoption).audclaim with a non-empty expected list is a rejection.Sample usage
Tests
Added three new unit-test classes — none require a live signing key or project:
VerifyOptionsTest(7 tests) — builder + helpers, null/empty handling, defaults.JwtUtilsAudienceTest(9 tests) — mocksClaims.getAudience()to exerciseJwtUtils.verifyAudiencein isolation: null options, empty list, single match, mismatch throws, missing claim throws, null claim throws, multi-expected any-match, multi-token any-match, none-match throws.AuthenticationServiceVerifyOptionsTest(5 tests) — input-validation parity: the new overloads reject blank inputs identically to the legacy ones.Constraints respected
Gap analysis vs. node-sdk (follow-up work)
VerifyOptions/audwas the gap called out by the reporter, and this PR addresses it fully. A broader parity pass against node-sdk is recommended as follow-up — a detailed list of remaining differences (OAuth flows, WebAuthn options, some management APIs, cookie-helper utilities, etc.) is included in the PR changelog markdown delivered alongside this PR. Those are intentionally out of scope here so this change stays reviewable.Draft
Opened as a draft per the reporter's request. Ready for early review on the API surface; I'll flip to ready-for-review once CI is green and any naming / shape feedback is addressed.
🤖 Generated with Claude Code