1616package dev .sigstore .oidc .client ;
1717
1818import com .google .api .client .auth .oauth2 .AuthorizationCodeFlow ;
19+ import com .google .api .client .auth .oauth2 .AuthorizationCodeRequestUrl ;
1920import com .google .api .client .auth .oauth2 .BearerToken ;
2021import com .google .api .client .auth .oauth2 .ClientParametersAuthentication ;
2122import com .google .api .client .auth .openidconnect .IdToken ;
3839import dev .sigstore .trustroot .Service ;
3940import java .io .IOException ;
4041import java .net .URI ;
42+ import java .security .SecureRandom ;
4143import java .util .Arrays ;
44+ import java .util .Base64 ;
4245import java .util .Locale ;
4346import java .util .Map ;
4447import java .util .logging .Logger ;
@@ -153,6 +156,10 @@ public OidcToken getIDToken(Map<String, String> env) throws OidcException {
153156 throw new OidcException (
154157 "ioexception obtaining and parsing oidc configuration for " + issuer , e );
155158 }
159+
160+ // Generate a cryptographically secure nonce for replay attack prevention
161+ String nonce = generateNonce ();
162+
156163 AuthorizationCodeFlow .Builder flowBuilder =
157164 new AuthorizationCodeFlow .Builder (
158165 BearerToken .authorizationHeaderAccessMethod (),
@@ -171,9 +178,12 @@ public OidcToken getIDToken(Map<String, String> env) throws OidcException {
171178 memStoreFactory
172179 .getDataStore ("user" )
173180 .set (ID_TOKEN_KEY , tokenResponse .get (ID_TOKEN_KEY ).toString ()));
181+
182+ // Use custom flow that injects nonce into authorization URL
183+ NonceAuthorizationCodeFlow flow = new NonceAuthorizationCodeFlow (flowBuilder , nonce );
174184 AuthorizationCodeInstalledApp app =
175185 new AuthorizationCodeInstalledApp (
176- flowBuilder . build () , new LocalServerReceiver (), browserHandler ::openBrowser );
186+ flow , new LocalServerReceiver (), browserHandler ::openBrowser );
177187
178188 String idTokenString = null ;
179189 IdToken parsedIdToken = null ;
@@ -189,6 +199,16 @@ public OidcToken getIDToken(Map<String, String> env) throws OidcException {
189199 if (!idTokenVerifier .verifyOrThrow (parsedIdToken )) {
190200 throw new OidcException ("id token could not be verified" );
191201 }
202+
203+ // Verify that the nonce in the ID token matches the one we sent
204+ Object tokenNonce = parsedIdToken .getPayload ().get ("nonce" );
205+ if (tokenNonce == null ) {
206+ throw new OidcException ("id token is missing required nonce claim" );
207+ }
208+ if (!nonce .equals (tokenNonce .toString ())) {
209+ throw new OidcException (
210+ "nonce in id token does not match expected value - possible replay attack" );
211+ }
192212 } catch (IOException e ) {
193213 // TODO: maybe a more descriptive exception message
194214 throw new OidcException ("ioexception during oidc handshake" , e );
@@ -254,4 +274,39 @@ public interface BrowserHandler {
254274 /** Opens a browser to allow a user to complete the oauth browser workflow. */
255275 void openBrowser (String url ) throws IOException ;
256276 }
277+
278+ /**
279+ * Generates a cryptographically secure random nonce for OIDC authentication. The nonce is used to
280+ * prevent replay attacks by binding the ID token to the authentication request.
281+ *
282+ * @return a URL-safe base64-encoded random string
283+ */
284+ private static String generateNonce () {
285+ SecureRandom secureRandom = new SecureRandom ();
286+ byte [] nonceBytes = new byte [32 ];
287+ secureRandom .nextBytes (nonceBytes );
288+ return Base64 .getUrlEncoder ().withoutPadding ().encodeToString (nonceBytes );
289+ }
290+
291+ /**
292+ * Custom AuthorizationCodeFlow that adds a nonce parameter to the authorization URL. This is
293+ * required for OpenID Connect to prevent replay attacks.
294+ */
295+ private static class NonceAuthorizationCodeFlow extends AuthorizationCodeFlow {
296+ private final String nonce ;
297+
298+ NonceAuthorizationCodeFlow (AuthorizationCodeFlow .Builder builder , String nonce ) {
299+ super (builder );
300+ this .nonce = nonce ;
301+ }
302+
303+ @ Override
304+ public AuthorizationCodeRequestUrl newAuthorizationUrl () {
305+ return super .newAuthorizationUrl ().set ("nonce" , nonce );
306+ }
307+
308+ String getNonce () {
309+ return nonce ;
310+ }
311+ }
257312}
0 commit comments