55import com .auth0 .exception .Auth0Exception ;
66import com .auth0 .json .auth .TokenHolder ;
77import com .auth0 .net .Telemetry ;
8- import com .fasterxml .jackson .core .type .TypeReference ;
9- import com .fasterxml .jackson .databind .ObjectMapper ;
108import com .google .common .annotations .VisibleForTesting ;
119
1210import javax .servlet .http .HttpServletRequest ;
1311import javax .servlet .http .HttpServletResponse ;
1412import java .util .Arrays ;
1513import java .util .List ;
16- import java .util .Map ;
1714
1815import static com .auth0 .InvalidRequestException .*;
1916
@@ -183,7 +180,6 @@ AuthAPI createClientForDomain(String domain) {
183180 setupTelemetry (client );
184181 }
185182
186- System .out .println ("Created dynamic AuthAPI for domain: " + domain + " " + clientId );
187183 return client ;
188184 }
189185
@@ -231,24 +227,12 @@ AuthorizeUrl buildAuthorizeUrl(HttpServletRequest request, HttpServletResponse r
231227 }
232228
233229 // null response means state and nonce will be stored in session, so legacy
234- // cookie flag does not apply
230+ // cookie flag does not apply and origin domain cookie cannot be set
235231 if (response != null ) {
236232 creator .withLegacySameSiteCookie (useLegacySameSiteCookie );
233+ creator .withOriginDomain (originDomain , clientSecret );
237234 }
238235
239- boolean isSecure = request .isSecure ();
240-
241- TransientCookieStore .storeOriginData (
242- response ,
243- originDomain ,
244- SameSite .LAX ,
245- constructIssuer (originDomain ),
246- cookiePath ,
247- isSecure );
248-
249- TransientCookieStore .storeOriginData (response , originDomain , SameSite .LAX , constructIssuer (originDomain ), cookiePath ,
250- isSecure );
251-
252236 return getAuthorizeUrl (nonce , creator );
253237 }
254238
@@ -269,19 +253,21 @@ Tokens process(HttpServletRequest request, HttpServletResponse response) throws
269253 assertNoError (request );
270254 assertValidState (request , response );
271255
272- // Retrieve stored origin domain and issuer from the authorization flow
273- String originDomain = TransientCookieStore .getOriginDomain (request , response );
274- String originIssuer = TransientCookieStore .getOriginIssuer (request , response );
256+ // Extract origin_domain from the HMAC-signed transaction state cookie.
257+ // If the cookie was tampered with, getSignedOriginDomain returns null.
258+ String originDomain = null ;
259+ if (response != null ) {
260+ originDomain = TransientCookieStore .getSignedOriginDomain (request , response , clientSecret );
261+ }
275262
263+ // Fallback for session-based (deprecated) flow or if cookie was not set
276264 if (originDomain == null ) {
277265 originDomain = domainProvider .getDomain (request );
278266 }
279267
280- if (originIssuer == null ) {
281- originIssuer = constructIssuer (originDomain );
282- }
268+ // Always derive the issuer from the verified domain — never from a cookie
269+ String originIssuer = constructIssuer (originDomain );
283270
284- // Each request will create its own verification options with the correct issuer
285271 Tokens frontChannelTokens = getFrontChannelTokens (request , originDomain , originIssuer );
286272 List <String > responseTypeList = getResponseType ();
287273
@@ -322,18 +308,24 @@ private Tokens getVerifiedTokens(HttpServletRequest request, HttpServletResponse
322308 Tokens codeExchangeTokens = null ;
323309
324310 // Get nonce for this specific request
325- String nonce = response != null
326- ? (TransientCookieStore .getNonce (request , response ) != null
327- ? TransientCookieStore .getNonce (request , response )
328- : RandomStorage .removeSessionNonce (request ))
329- : RandomStorage .removeSessionNonce (request );
311+ String nonce ;
312+ if (response != null ) {
313+ nonce = TransientCookieStore .getNonce (request , response );
314+ // Fallback to session if cookie was not set (deprecated API path)
315+ if (nonce == null ) {
316+ nonce = RandomStorage .removeSessionNonce (request );
317+ }
318+ } else {
319+ nonce = RandomStorage .removeSessionNonce (request );
320+ }
330321
331322 IdTokenVerifier .Options requestVerifyOptions = createRequestVerifyOptions (originIssuer , nonce );
332323
333324 try {
334325 if (responseTypeList .contains (KEY_ID_TOKEN )) {
335- // Implicit/Hybrid flow: must verify front-channel ID Token first
336- validateIdTokenIssuer (frontChannelTokens .getIdToken (), originIssuer );
326+ // Implicit/Hybrid flow: must verify front-channel ID Token first.
327+ // The issuer is derived from the HMAC-verified domain, so this check
328+ // validates the token's iss against a trusted value.
337329 tokenVerifier .verify (frontChannelTokens .getIdToken (), requestVerifyOptions );
338330 }
339331 if (responseTypeList .contains (KEY_CODE )) {
@@ -344,7 +336,6 @@ private Tokens getVerifiedTokens(HttpServletRequest request, HttpServletResponse
344336 // If we already verified the front-channel token, don't verify it again.
345337 String idTokenFromCodeExchange = codeExchangeTokens .getIdToken ();
346338 if (idTokenFromCodeExchange != null ) {
347- validateIdTokenIssuer (idTokenFromCodeExchange , originIssuer );
348339 tokenVerifier .verify (idTokenFromCodeExchange , requestVerifyOptions );
349340 }
350341 }
@@ -384,66 +375,6 @@ private IdTokenVerifier.Options createRequestVerifyOptions(String issuer, String
384375 return requestOptions ;
385376 }
386377
387- /**
388- * Validates that the ID Token's issuer matches the expected origin issuer.
389- *
390- * @param idToken the ID Token to validate
391- * @param expectedIssuer the expected issuer from the authorization flow
392- * @throws IdentityVerificationException if the issuer doesn't match
393- */
394- private void validateIdTokenIssuer (String idToken , String expectedIssuer ) throws IdentityVerificationException {
395- if (idToken == null || expectedIssuer == null ) {
396- return ;
397- }
398-
399- try {
400- String [] parts = idToken .split ("\\ ." );
401- if (parts .length != 3 ) {
402- throw new IdentityVerificationException (JWT_VERIFICATION_ERROR , "Invalid ID Token format" , null );
403- }
404-
405- String payload = new String (java .util .Base64 .getUrlDecoder ().decode (parts [1 ]));
406- String tokenIssuer = extractIssuerFromPayload (payload );
407-
408- if (!tokenIssuer .equals (expectedIssuer )) {
409- throw new IdentityVerificationException (JWT_VERIFICATION_ERROR ,
410- String .format ("Token issuer '%s' does not match expected issuer '%s'" ,
411- tokenIssuer , expectedIssuer ),
412- null );
413- }
414- } catch (Exception e ) {
415- if (e instanceof IdentityVerificationException ) {
416- throw e ;
417- }
418- throw new IdentityVerificationException (JWT_VERIFICATION_ERROR ,
419- "Failed to validate token issuer: " + e .getMessage (), e );
420- }
421- }
422-
423- /**
424- * Extracts the issuer (iss) claim from the ID Token payload.
425- *
426- * @param payload the decoded payload of the ID Token
427- * @return the issuer claim value
428- * @throws IdentityVerificationException if the issuer claim is missing
429- */
430- private String extractIssuerFromPayload (String payload ) throws IdentityVerificationException {
431- try {
432- Map <String , Object > payloadMap = new ObjectMapper ().readValue (payload ,
433- new TypeReference <Map <String , Object >>() {
434- });
435- if (payloadMap .containsKey ("iss" )) {
436- return payloadMap .get ("iss" ).toString ();
437- } else {
438- throw new IdentityVerificationException (JWT_VERIFICATION_ERROR ,
439- "Issuer claim (iss) is missing in the ID Token payload." , null );
440- }
441- } catch (Exception e ) {
442- throw new IdentityVerificationException (JWT_VERIFICATION_ERROR ,
443- "Failed to parse ID Token payload: " + e .getMessage (), e );
444- }
445- }
446-
447378 List <String > getResponseType () {
448379 return Arrays .asList (responseType .split (" " ));
449380 }
0 commit comments