From 81cdc85d7c480cdf76f5845437b9b2bb53e11e6e Mon Sep 17 00:00:00 2001 From: Kailash B Date: Fri, 17 Jul 2026 16:38:00 +0530 Subject: [PATCH 1/2] feat: add access token exchange and generic subject token type to Token Vault --- EXAMPLES.md | 52 +++++ .../java/com/auth0/client/auth/AuthAPI.java | 116 +++++++++- .../com/auth0/client/auth/AuthAPITest.java | 204 +++++++++++++++++- 3 files changed, 357 insertions(+), 15 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 67608c553..c5731b7ad 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -5,6 +5,7 @@ - [Management API usage](#management-api-usage) - [Verifying an ID token](#verifying-an-id-token) - [Organizations](#organizations) +- [Token Vault](#token-vault) - [Logging](#logging) - [Asynchronous operations](#asynchronous-operations) @@ -325,6 +326,57 @@ String url = auth.authorizeUrl("https://me.auth0.com/callback") .build(); ``` +## Token Vault + +[Token Vault](https://auth0.com/docs/secure/tokens/token-vault) lets you exchange an Auth0 token for a federated identity provider's access token, so your application can call the provider's APIs on the user's behalf. The connection must have Token Vault enabled, and because this exchange requires a private client, the `AuthAPI` instance must be configured with a client secret or client assertion. + +### Exchange a refresh token + +Use `getTokenForConnectionWithRefreshToken()` to exchange an Auth0 refresh token for the federated provider's access token: + +```java +AuthAPI auth = AuthAPI.newBuilder("{YOUR_DOMAIN}", "{YOUR_CLIENT_ID}", "{YOUR_CLIENT_SECRET}").build(); +TokenHolder result = auth.getTokenForConnectionWithRefreshToken("google-oauth2", "{REFRESH_TOKEN}", null) + .execute() + .getBody(); +String federatedAccessToken = result.getAccessToken(); +``` + +### Exchange an access token + +Use `getTokenForConnectionWithAccessToken()` to exchange an Auth0 access token instead: + +```java +TokenHolder result = auth.getTokenForConnectionWithAccessToken("google-oauth2", "{ACCESS_TOKEN}", null) + .execute() + .getBody(); +String federatedAccessToken = result.getAccessToken(); +``` + +### Specifying a login hint + +When a user has multiple accounts on the same connection, pass their ID within the identity provider as the `loginHint` (the final argument). Pass `null` to omit it: + +```java +TokenHolder result = auth.getTokenForConnectionWithRefreshToken("google-oauth2", "{REFRESH_TOKEN}", "{GOOGLE_USER_ID}") + .execute() + .getBody(); +``` + +### Exchanging other token types + +For full control over the subject token type, use the generic `getTokenForConnection()` and supply the `subject_token_type` URN yourself: + +```java +TokenHolder result = auth.getTokenForConnection( + "google-oauth2", + "{SUBJECT_TOKEN}", + "urn:ietf:params:oauth:token-type:refresh_token", + "{GOOGLE_USER_ID}") + .execute() + .getBody(); +``` + ## Logging The SDK is silent by default. You can enable logging to see HTTP requests and responses, which is useful for debugging. diff --git a/src/main/java/com/auth0/client/auth/AuthAPI.java b/src/main/java/com/auth0/client/auth/AuthAPI.java index 342e70ca6..b6e874680 100644 --- a/src/main/java/com/auth0/client/auth/AuthAPI.java +++ b/src/main/java/com/auth0/client/auth/AuthAPI.java @@ -843,15 +843,20 @@ public TokenRequest exchangeToken(String subjectToken, String subjectTokenType) } /** - * Creates a request to exchange an Auth0 refresh token for a federated identity provider's access token + * Creates a request to exchange a subject token for a federated identity provider's access token * using the Token Vault grant * {@code urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token}. + * This is the generic entry point: the caller supplies both the {@code subjectToken} and its + * {@code subjectTokenType}, so no assumption is made about which kind of token is being exchanged. + * For the common cases, prefer {@link #getTokenForConnectionWithRefreshToken(String, String, String)} + * or {@link #getTokenForConnectionWithAccessToken(String, String, String)}. * The connection must have the token vault enabled, and client authentication * (client secret or client assertion) is required as this must be a private client. *
      * {@code
      * try {
-     *      TokenHolder result = authAPI.getTokenForConnection("google-oauth2", refreshToken, "google-user-id")
+     *      TokenHolder result = authAPI.getTokenForConnection(
+     *              "google-oauth2", subjectToken, "urn:ietf:params:oauth:token-type:refresh_token", "google-user-id")
      *          .execute()
      *          .getBody();
      *      String federatedAccessToken = result.getAccessToken();
@@ -864,22 +869,27 @@ public TokenRequest exchangeToken(String subjectToken, String subjectTokenType)
      * @see Token Vault documentation
      * @param connection the name of the federated connection to obtain an access token for
      *                   (for example {@code google-oauth2}). Must not be null.
-     * @param refreshToken a valid Auth0 refresh token to exchange. Must not be null.
+     * @param subjectToken the Auth0 token to exchange. Must not be null.
+     * @param subjectTokenType the identifier for the type of {@code subjectToken}, for example
+     *                         {@code urn:ietf:params:oauth:token-type:refresh_token} or
+     *                         {@code urn:ietf:params:oauth:token-type:access_token}. Must not be null.
      * @param loginHint the user's ID within the identity provider specified by the connection
      *                  (for example, the Google user ID when the connection is {@code google-oauth2}).
      *                  May be null, in which case no {@code login_hint} is sent.
      * @return a Request to configure and execute.
      */
-    public TokenRequest getTokenForConnection(String connection, String refreshToken, String loginHint) {
+    public TokenRequest getTokenForConnection(
+            String connection, String subjectToken, String subjectTokenType, String loginHint) {
         Asserts.assertNotNull(connection, "connection");
-        Asserts.assertNotNull(refreshToken, "refresh token");
+        Asserts.assertNotNull(subjectToken, "subject token");
+        Asserts.assertNotNull(subjectTokenType, "subject token type");
 
         TokenRequest request = new TokenRequest(client, getTokenUrl());
         request.addParameter(KEY_CLIENT_ID, clientId);
         request.addParameter(
                 KEY_GRANT_TYPE, "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token");
-        request.addParameter(KEY_SUBJECT_TOKEN, refreshToken);
-        request.addParameter(KEY_SUBJECT_TOKEN_TYPE, "urn:ietf:params:oauth:token-type:refresh_token");
+        request.addParameter(KEY_SUBJECT_TOKEN, subjectToken);
+        request.addParameter(KEY_SUBJECT_TOKEN_TYPE, subjectTokenType);
         request.addParameter(
                 KEY_REQUESTED_TOKEN_TYPE, "http://auth0.com/oauth/token-type/federated-connection-access-token");
         request.addParameter(KEY_CONNECTION, connection);
@@ -890,6 +900,98 @@ public TokenRequest getTokenForConnection(String connection, String refreshToken
         return request;
     }
 
+    /**
+     * Convenience method to exchange an Auth0 refresh token for a federated identity provider's access token
+     * using the Token Vault grant. Delegates to
+     * {@link #getTokenForConnection(String, String, String, String)} with the subject token type
+     * {@code urn:ietf:params:oauth:token-type:refresh_token}.
+     * The connection must have the token vault enabled, and client authentication
+     * (client secret or client assertion) is required as this must be a private client.
+     * 
+     * {@code
+     * try {
+     *      TokenHolder result = authAPI.getTokenForConnectionWithRefreshToken("google-oauth2", refreshToken, "google-user-id")
+     *          .execute()
+     *          .getBody();
+     *      String federatedAccessToken = result.getAccessToken();
+     * } catch (Auth0Exception e) {
+     *      //Something happened
+     * }
+     * }
+     * 
+ * + * @see Token Vault documentation + * @param connection the name of the federated connection to obtain an access token for + * (for example {@code google-oauth2}). Must not be null. + * @param refreshToken a valid Auth0 refresh token to exchange. Must not be null. + * @param loginHint the user's ID within the identity provider specified by the connection + * (for example, the Google user ID when the connection is {@code google-oauth2}). + * May be null, in which case no {@code login_hint} is sent. + * @return a Request to configure and execute. + */ + public TokenRequest getTokenForConnectionWithRefreshToken( + String connection, String refreshToken, String loginHint) { + Asserts.assertNotNull(refreshToken, "refresh token"); + return getTokenForConnection( + connection, refreshToken, "urn:ietf:params:oauth:token-type:refresh_token", loginHint); + } + + /** + * Convenience method to exchange an Auth0 access token for a federated identity provider's access token + * using the Token Vault grant. Delegates to + * {@link #getTokenForConnection(String, String, String, String)} with the subject token type + * {@code urn:ietf:params:oauth:token-type:access_token}. + * The connection must have the token vault enabled, and client authentication + * (client secret or client assertion) is required as this must be a private client. + *
+     * {@code
+     * try {
+     *      TokenHolder result = authAPI.getTokenForConnectionWithAccessToken("google-oauth2", accessToken, "google-user-id")
+     *          .execute()
+     *          .getBody();
+     *      String federatedAccessToken = result.getAccessToken();
+     * } catch (Auth0Exception e) {
+     *      //Something happened
+     * }
+     * }
+     * 
+ * + * @see Token Vault documentation + * @param connection the name of the federated connection to obtain an access token for + * (for example {@code google-oauth2}). Must not be null. + * @param accessToken a valid Auth0 access token to exchange. Must not be null. + * @param loginHint the user's ID within the identity provider specified by the connection + * (for example, the Google user ID when the connection is {@code google-oauth2}). + * May be null, in which case no {@code login_hint} is sent. + * @return a Request to configure and execute. + */ + public TokenRequest getTokenForConnectionWithAccessToken( + String connection, String accessToken, String loginHint) { + Asserts.assertNotNull(accessToken, "access token"); + return getTokenForConnection( + connection, accessToken, "urn:ietf:params:oauth:token-type:access_token", loginHint); + } + + /** + * Creates a request to exchange an Auth0 refresh token for a federated identity provider's access token + * using the Token Vault grant. + * + * @deprecated Use {@link #getTokenForConnectionWithRefreshToken(String, String, String)} to exchange a + * refresh token, or {@link #getTokenForConnectionWithAccessToken(String, String, String)} to + * exchange an access token. For full control over the subject token type, use + * {@link #getTokenForConnection(String, String, String, String)}. + * @param connection the name of the federated connection to obtain an access token for + * (for example {@code google-oauth2}). Must not be null. + * @param refreshToken a valid Auth0 refresh token to exchange. Must not be null. + * @param loginHint the user's ID within the identity provider specified by the connection. + * May be null, in which case no {@code login_hint} is sent. + * @return a Request to configure and execute. + */ + @Deprecated + public TokenRequest getTokenForConnection(String connection, String refreshToken, String loginHint) { + return getTokenForConnectionWithRefreshToken(connection, refreshToken, loginHint); + } + /** * Creates a request to revoke an existing Refresh Token. * Confidential clients (Regular Web Apps) must have a client secret configured on this {@code AuthAPI} instance. diff --git a/src/test/java/com/auth0/client/auth/AuthAPITest.java b/src/test/java/com/auth0/client/auth/AuthAPITest.java index 7278a80ef..ccba633f3 100644 --- a/src/test/java/com/auth0/client/auth/AuthAPITest.java +++ b/src/test/java/com/auth0/client/auth/AuthAPITest.java @@ -1041,21 +1041,111 @@ public void shouldCreateTokenExchangeRequestWithClientAssertion() throws Excepti public void shouldThrowOnGetTokenForConnectionWithNullConnection() { verifyThrows( IllegalArgumentException.class, - () -> api.getTokenForConnection(null, "test-refresh-token", null), + () -> api.getTokenForConnection( + null, "test-token", "urn:ietf:params:oauth:token-type:refresh_token", null), "'connection' cannot be null!"); } @Test - public void shouldThrowOnGetTokenForConnectionWithNullRefreshToken() { + public void shouldThrowOnGetTokenForConnectionWithNullSubjectToken() { verifyThrows( IllegalArgumentException.class, - () -> api.getTokenForConnection("google-oauth2", null, null), + () -> api.getTokenForConnection( + "google-oauth2", null, "urn:ietf:params:oauth:token-type:refresh_token", null), + "'subject token' cannot be null!"); + } + + @Test + public void shouldThrowOnGetTokenForConnectionWithNullSubjectTokenType() { + verifyThrows( + IllegalArgumentException.class, + () -> api.getTokenForConnection("google-oauth2", "test-token", null, null), + "'subject token type' cannot be null!"); + } + + @Test + public void shouldCreateGetTokenForConnectionRequestWithCustomSubjectTokenType() throws Exception { + TokenRequest request = api.getTokenForConnection( + "google-oauth2", "test-token", "urn:example:token-type:custom", null); + assertThat(request, is(notNullValue())); + + server.jsonResponse(AUTH_TOKENS, 200); + TokenHolder response = request.execute().getBody(); + RecordedRequest recordedRequest = server.takeRequest(); + + assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/oauth/token")); + assertThat(recordedRequest, hasHeader("Content-Type", "application/json")); + + Map body = bodyFromRequest(recordedRequest); + assertThat( + body, + hasEntry( + "grant_type", + "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token")); + assertThat(body, hasEntry("client_id", CLIENT_ID)); + assertThat(body, hasEntry("client_secret", CLIENT_SECRET)); + assertThat(body, hasEntry("subject_token", "test-token")); + assertThat(body, hasEntry("subject_token_type", "urn:example:token-type:custom")); + assertThat( + body, + hasEntry( + "requested_token_type", "http://auth0.com/oauth/token-type/federated-connection-access-token")); + assertThat(body, hasEntry("connection", "google-oauth2")); + assertThat(body, not(hasKey("login_hint"))); + + assertThat(response, is(notNullValue())); + assertThat(response.getAccessToken(), not(emptyOrNullString())); + } + + @Test + public void shouldCreateGetTokenForConnectionRequestWithLoginHint() throws Exception { + TokenRequest request = api.getTokenForConnection( + "google-oauth2", "test-token", "urn:example:token-type:custom", "google-user-id"); + assertThat(request, is(notNullValue())); + + server.jsonResponse(AUTH_TOKENS, 200); + TokenHolder response = request.execute().getBody(); + RecordedRequest recordedRequest = server.takeRequest(); + + Map body = bodyFromRequest(recordedRequest); + assertThat(body, hasEntry("subject_token", "test-token")); + assertThat(body, hasEntry("subject_token_type", "urn:example:token-type:custom")); + assertThat(body, hasEntry("connection", "google-oauth2")); + assertThat(body, hasEntry("login_hint", "google-user-id")); + + assertThat(response, is(notNullValue())); + assertThat(response.getAccessToken(), not(emptyOrNullString())); + } + + @Test + public void getTokenForConnectionRequiresClientAuthentication() { + verifyThrows( + IllegalStateException.class, + () -> apiNoClientAuthentication.getTokenForConnection( + "google-oauth2", "test-token", "urn:ietf:params:oauth:token-type:refresh_token", null), + "A client secret or client assertion signing key is required for this operation"); + } + + @Test + public void shouldThrowOnGetTokenForConnectionWithRefreshTokenWithNullConnection() { + verifyThrows( + IllegalArgumentException.class, + () -> api.getTokenForConnectionWithRefreshToken(null, "test-refresh-token", null), + "'connection' cannot be null!"); + } + + @Test + public void shouldThrowOnGetTokenForConnectionWithRefreshTokenWithNullRefreshToken() { + verifyThrows( + IllegalArgumentException.class, + () -> api.getTokenForConnectionWithRefreshToken("google-oauth2", null, null), "'refresh token' cannot be null!"); } @Test - public void shouldCreateGetTokenForConnectionRequest() throws Exception { - TokenRequest request = api.getTokenForConnection("google-oauth2", "test-refresh-token", null); + public void shouldCreateGetTokenForConnectionWithRefreshTokenRequest() throws Exception { + TokenRequest request = + api.getTokenForConnectionWithRefreshToken("google-oauth2", "test-refresh-token", null); assertThat(request, is(notNullValue())); server.jsonResponse(AUTH_TOKENS, 200); @@ -1087,7 +1177,35 @@ public void shouldCreateGetTokenForConnectionRequest() throws Exception { } @Test - public void shouldCreateGetTokenForConnectionRequestWithLoginHint() throws Exception { + public void shouldCreateGetTokenForConnectionWithRefreshTokenRequestWithLoginHint() throws Exception { + TokenRequest request = + api.getTokenForConnectionWithRefreshToken("google-oauth2", "test-refresh-token", "google-user-id"); + assertThat(request, is(notNullValue())); + + server.jsonResponse(AUTH_TOKENS, 200); + TokenHolder response = request.execute().getBody(); + RecordedRequest recordedRequest = server.takeRequest(); + + Map body = bodyFromRequest(recordedRequest); + assertThat(body, hasEntry("connection", "google-oauth2")); + assertThat(body, hasEntry("login_hint", "google-user-id")); + + assertThat(response, is(notNullValue())); + assertThat(response.getAccessToken(), not(emptyOrNullString())); + } + + @Test + public void getTokenForConnectionWithRefreshTokenRequiresClientAuthentication() { + verifyThrows( + IllegalStateException.class, + () -> apiNoClientAuthentication.getTokenForConnectionWithRefreshToken( + "google-oauth2", "test-refresh-token", null), + "A client secret or client assertion signing key is required for this operation"); + } + + @Test + @SuppressWarnings("deprecation") + public void deprecatedGetTokenForConnectionExchangesRefreshToken() throws Exception { TokenRequest request = api.getTokenForConnection("google-oauth2", "test-refresh-token", "google-user-id"); assertThat(request, is(notNullValue())); @@ -1096,6 +1214,8 @@ public void shouldCreateGetTokenForConnectionRequestWithLoginHint() throws Excep RecordedRequest recordedRequest = server.takeRequest(); Map body = bodyFromRequest(recordedRequest); + assertThat(body, hasEntry("subject_token", "test-refresh-token")); + assertThat(body, hasEntry("subject_token_type", "urn:ietf:params:oauth:token-type:refresh_token")); assertThat(body, hasEntry("connection", "google-oauth2")); assertThat(body, hasEntry("login_hint", "google-user-id")); @@ -1104,10 +1224,78 @@ public void shouldCreateGetTokenForConnectionRequestWithLoginHint() throws Excep } @Test - public void getTokenForConnectionRequiresClientAuthentication() { + public void shouldThrowOnGetTokenForConnectionWithAccessTokenWithNullConnection() { + verifyThrows( + IllegalArgumentException.class, + () -> api.getTokenForConnectionWithAccessToken(null, "test-access-token", null), + "'connection' cannot be null!"); + } + + @Test + public void shouldThrowOnGetTokenForConnectionWithAccessTokenWithNullAccessToken() { + verifyThrows( + IllegalArgumentException.class, + () -> api.getTokenForConnectionWithAccessToken("google-oauth2", null, null), + "'access token' cannot be null!"); + } + + @Test + public void shouldCreateGetTokenForConnectionWithAccessTokenRequest() throws Exception { + TokenRequest request = api.getTokenForConnectionWithAccessToken("google-oauth2", "test-access-token", null); + assertThat(request, is(notNullValue())); + + server.jsonResponse(AUTH_TOKENS, 200); + TokenHolder response = request.execute().getBody(); + RecordedRequest recordedRequest = server.takeRequest(); + + assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/oauth/token")); + assertThat(recordedRequest, hasHeader("Content-Type", "application/json")); + + Map body = bodyFromRequest(recordedRequest); + assertThat( + body, + hasEntry( + "grant_type", + "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token")); + assertThat(body, hasEntry("client_id", CLIENT_ID)); + assertThat(body, hasEntry("client_secret", CLIENT_SECRET)); + assertThat(body, hasEntry("subject_token", "test-access-token")); + assertThat(body, hasEntry("subject_token_type", "urn:ietf:params:oauth:token-type:access_token")); + assertThat( + body, + hasEntry( + "requested_token_type", "http://auth0.com/oauth/token-type/federated-connection-access-token")); + assertThat(body, hasEntry("connection", "google-oauth2")); + assertThat(body, not(hasKey("login_hint"))); + + assertThat(response, is(notNullValue())); + assertThat(response.getAccessToken(), not(emptyOrNullString())); + } + + @Test + public void shouldCreateGetTokenForConnectionWithAccessTokenRequestWithLoginHint() throws Exception { + TokenRequest request = + api.getTokenForConnectionWithAccessToken("google-oauth2", "test-access-token", "google-user-id"); + assertThat(request, is(notNullValue())); + + server.jsonResponse(AUTH_TOKENS, 200); + TokenHolder response = request.execute().getBody(); + RecordedRequest recordedRequest = server.takeRequest(); + + Map body = bodyFromRequest(recordedRequest); + assertThat(body, hasEntry("connection", "google-oauth2")); + assertThat(body, hasEntry("login_hint", "google-user-id")); + + assertThat(response, is(notNullValue())); + assertThat(response.getAccessToken(), not(emptyOrNullString())); + } + + @Test + public void getTokenForConnectionWithAccessTokenRequiresClientAuthentication() { verifyThrows( IllegalStateException.class, - () -> apiNoClientAuthentication.getTokenForConnection("google-oauth2", "test-refresh-token", null), + () -> apiNoClientAuthentication.getTokenForConnectionWithAccessToken( + "google-oauth2", "test-access-token", null), "A client secret or client assertion signing key is required for this operation"); } From 374b953b320ee595aeca1ae2b6ac8ce9360f4cd7 Mon Sep 17 00:00:00 2001 From: Kailash B Date: Fri, 17 Jul 2026 16:48:18 +0530 Subject: [PATCH 2/2] chore: Fix spotless error --- src/main/java/com/auth0/client/auth/AuthAPI.java | 3 +-- src/test/java/com/auth0/client/auth/AuthAPITest.java | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/auth0/client/auth/AuthAPI.java b/src/main/java/com/auth0/client/auth/AuthAPI.java index b6e874680..c36975dfb 100644 --- a/src/main/java/com/auth0/client/auth/AuthAPI.java +++ b/src/main/java/com/auth0/client/auth/AuthAPI.java @@ -965,8 +965,7 @@ public TokenRequest getTokenForConnectionWithRefreshToken( * May be null, in which case no {@code login_hint} is sent. * @return a Request to configure and execute. */ - public TokenRequest getTokenForConnectionWithAccessToken( - String connection, String accessToken, String loginHint) { + public TokenRequest getTokenForConnectionWithAccessToken(String connection, String accessToken, String loginHint) { Asserts.assertNotNull(accessToken, "access token"); return getTokenForConnection( connection, accessToken, "urn:ietf:params:oauth:token-type:access_token", loginHint); diff --git a/src/test/java/com/auth0/client/auth/AuthAPITest.java b/src/test/java/com/auth0/client/auth/AuthAPITest.java index ccba633f3..d78699384 100644 --- a/src/test/java/com/auth0/client/auth/AuthAPITest.java +++ b/src/test/java/com/auth0/client/auth/AuthAPITest.java @@ -1065,8 +1065,8 @@ public void shouldThrowOnGetTokenForConnectionWithNullSubjectTokenType() { @Test public void shouldCreateGetTokenForConnectionRequestWithCustomSubjectTokenType() throws Exception { - TokenRequest request = api.getTokenForConnection( - "google-oauth2", "test-token", "urn:example:token-type:custom", null); + TokenRequest request = + api.getTokenForConnection("google-oauth2", "test-token", "urn:example:token-type:custom", null); assertThat(request, is(notNullValue())); server.jsonResponse(AUTH_TOKENS, 200); @@ -1144,8 +1144,7 @@ public void shouldThrowOnGetTokenForConnectionWithRefreshTokenWithNullRefreshTok @Test public void shouldCreateGetTokenForConnectionWithRefreshTokenRequest() throws Exception { - TokenRequest request = - api.getTokenForConnectionWithRefreshToken("google-oauth2", "test-refresh-token", null); + TokenRequest request = api.getTokenForConnectionWithRefreshToken("google-oauth2", "test-refresh-token", null); assertThat(request, is(notNullValue())); server.jsonResponse(AUTH_TOKENS, 200);