Add nonce support and the missing authorize/token request parameters - #67
Merged
AaronAtDuo merged 2 commits intoSep 2, 2026
Merged
Conversation
The Java SDK could validate a nonce on the returned ID token but had no way
to send one, so the feature was unreachable through Client. It was also the
only Duo Universal SDK omitting iss/aud from the signed authorize request and
client_id from the token POST.
Nonce:
* createAuthUrl(username, state, nonce) sends the nonce in the authorize
query string, matching duo_universal_python and Duo's documented precedence
that the query value wins over a JWT claim. The nonce is URL encoded so a
caller supplied value cannot introduce extra query parameters.
* exchangeAuthorizationCodeFor2FAResult(duoCode, username, nonce) builds the
existing DuoIdTokenValidator nonce constructor, which rejects an ID token
whose nonce claim does not match.
* Validator.validateNonce enforces Duo's documented 16 to 1024 characters,
inclusive on both ends.
* Token exposes the nonce claim alongside amr.
The two argument overloads delegate with a null nonce, so existing callers
send no nonce and behave exactly as before.
Request parameters:
* The authorize request JWT now carries iss (the client id) and aud
(https://{api_host}).
* The token POST now sends client_id as a form field.
The example app generates a nonce, stores it next to the state, and passes it
to both calls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
scweber-cisco
marked this pull request as ready for review
September 1, 2026 20:41
DuoConnector is public, so adding client_id to exchangeAuthorizationCodeFor2FAResult was source breaking for anyone calling the connector directly instead of going through Client. Restore the six argument signature as an overload that delegates with a null client_id; Retrofit drops null form fields, so it sends exactly what it sent before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jeffreyparker
approved these changes
Sep 2, 2026
AaronAtDuo
approved these changes
Sep 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Closes the gap between what Duo's OIDC endpoints accept and what this SDK sends, for three parameters.
Nonce
The SDK could already validate a nonce —
DuoIdTokenValidatorhas a nonce constructor and enforces the claim — butClienthad no way to send one, so the feature was unreachable through the public API. This adds the send side:createAuthUrl(username, state, nonce)puts the nonce in the authorize query string. That matchesduo_universal_pythonand Duo's documented precedence rule that a query value wins over the same value in the signed request JWT. The nonce is URL encoded, so a caller-supplied value containing&cannot introduce additional query parameters.exchangeAuthorizationCodeFor2FAResult(duoCode, username, nonce)builds the existing 5-argDuoIdTokenValidator, which fails validation if the ID token'snonceclaim doesn't match.Validator.validateNonceenforces Duo's documented 16–1024 characters, inclusive on both ends.Tokenexposes thenonceclaim, alongsideamr.The existing 2-arg overloads delegate with a
nullnonce, so current callers send no nonce and behave exactly as before.Note on what this buys us:
preferred_usernameis already checked unconditionally, so the nonce is not protecting against cross-user token substitution here. What it adds is binding the ID token to this authorization request, which is defense against same-user replay of a previously issued token.iss / aud on the authorize request
The signed request JWT now carries
iss(the client id) andaud(https://{api_host}). Both are optional per Duo's docs, but every other Duo Universal SDK sends them and this was the only one that didn't.client_id on the token POST
/oauth/v1/tokennow receivesclient_idas a form field, again matching the other SDKs.API surface
New public methods, no removals:
Client.createAuthUrl(String, String, String)Client.exchangeAuthorizationCodeFor2FAResult(String, String, String)Token.getNonce()/setNonce(String)DuoConnector.exchangeAuthorizationCodeFor2FAResult(..., String clientId)DuoConnector.exchangeAuthorizationCodeFor2FAResultneeded theclientId, and it's public, so the six-argument signature is retained as an overload that delegates with anullclient id. Retrofit drops null@Fields from the form body, so anyone calling the connector directly keeps both source compatibility and their exact previous request.Testing
Built with TDD; every new behavior has a test that was watched failing first.
mvn install— 66 tests, 0 failures, checkstyle clean on both modules.nonce&redirect_uri=evilinject a secondredirect_uri), the 16/1024 length boundaries and both out-of-range cases,iss/audclaims,client_idon the token POST, ID token rejection on nonce mismatch and acceptance on match, andTokennonce equality/toString.The
duo-exampleapp was updated to generate a nonce, store it next to the state, and pass it to both calls — so the demo exercises the new path end to end. I also ran theduo-exampleapp to confirm that it successfully completes with a nonce, and that if the nonce doesn't match, an error is received.🤖 Generated with Claude Code