From 14cf0b1903a2652ab99f05d0807bfa9467e1d089 Mon Sep 17 00:00:00 2001 From: Kailash B Date: Thu, 9 Jul 2026 16:02:21 +0530 Subject: [PATCH 1/3] feat: Adds Token Vault support in AuthApi --- .../java/com/auth0/client/auth/AuthAPI.java | 48 +++++++++++++ src/main/java/com/auth0/net/TokenRequest.java | 13 ++++ .../com/auth0/client/auth/AuthAPITest.java | 72 +++++++++++++++++++ 3 files changed, 133 insertions(+) diff --git a/src/main/java/com/auth0/client/auth/AuthAPI.java b/src/main/java/com/auth0/client/auth/AuthAPI.java index acdb3c83e..e58e4caec 100644 --- a/src/main/java/com/auth0/client/auth/AuthAPI.java +++ b/src/main/java/com/auth0/client/auth/AuthAPI.java @@ -59,6 +59,13 @@ public class AuthAPI { private static final String KEY_CLIENT_ASSERTION_TYPE = "client_assertion_type"; private static final String KEY_SUBJECT_TOKEN = "subject_token"; private static final String KEY_SUBJECT_TOKEN_TYPE = "subject_token_type"; + private static final String KEY_REQUESTED_TOKEN_TYPE = "requested_token_type"; + private static final String GRANT_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN = + "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token"; + private static final String SUBJECT_TOKEN_TYPE_REFRESH_TOKEN = + "urn:ietf:params:oauth:token-type:refresh_token"; + private static final String REQUESTED_TOKEN_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN = + "http://auth0.com/oauth/token-type/federated-connection-access-token"; private static final String PATH_OAUTH = "oauth"; private static final String PATH_TOKEN = "token"; private static final String PATH_DBCONNECTIONS = "dbconnections"; @@ -840,6 +847,47 @@ public TokenRequest exchangeToken(String subjectToken, String subjectTokenType) return request; } + /** + * Creates a request to exchange an Auth0 refresh 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}. + * 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)
+     *          .setLoginHint("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. + * @return a Request to configure and execute. + */ + public TokenRequest getTokenForConnection(String connection, String refreshToken) { + Asserts.assertNotNull(connection, "connection"); + Asserts.assertNotNull(refreshToken, "refresh token"); + + TokenRequest request = new TokenRequest(client, getTokenUrl()); + request.addParameter(KEY_CLIENT_ID, clientId); + request.addParameter(KEY_GRANT_TYPE, GRANT_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN); + request.addParameter(KEY_SUBJECT_TOKEN, refreshToken); + request.addParameter(KEY_SUBJECT_TOKEN_TYPE, SUBJECT_TOKEN_TYPE_REFRESH_TOKEN); + request.addParameter(KEY_REQUESTED_TOKEN_TYPE, REQUESTED_TOKEN_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN); + request.addParameter(KEY_CONNECTION, connection); + addClientAuthentication(request, true); + return request; + } + /** * 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/main/java/com/auth0/net/TokenRequest.java b/src/main/java/com/auth0/net/TokenRequest.java index 800c5f4a3..ca94f4266 100644 --- a/src/main/java/com/auth0/net/TokenRequest.java +++ b/src/main/java/com/auth0/net/TokenRequest.java @@ -46,4 +46,17 @@ public TokenRequest setScope(String scope) { super.addParameter("scope", scope); return this; } + + /** + * Setter for the login hint value to request. For the Token Vault federated connection + * token exchange, this is 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}). + * + * @param loginHint the login hint to request + * @return this request instance. + */ + public TokenRequest setLoginHint(String loginHint) { + super.addParameter("login_hint", loginHint); + return this; + } } diff --git a/src/test/java/com/auth0/client/auth/AuthAPITest.java b/src/test/java/com/auth0/client/auth/AuthAPITest.java index 8516480c0..00023f674 100644 --- a/src/test/java/com/auth0/client/auth/AuthAPITest.java +++ b/src/test/java/com/auth0/client/auth/AuthAPITest.java @@ -1035,6 +1035,78 @@ public void shouldCreateTokenExchangeRequestWithClientAssertion() throws Excepti assertThat(response.getAccessToken(), not(emptyOrNullString())); } + // Token Vault - Federated Connection Access Token + + @Test + public void shouldThrowOnGetTokenForConnectionWithNullConnection() { + verifyThrows( + IllegalArgumentException.class, + () -> api.getTokenForConnection(null, "test-refresh-token"), + "'connection' cannot be null!"); + } + + @Test + public void shouldThrowOnGetTokenForConnectionWithNullRefreshToken() { + verifyThrows( + IllegalArgumentException.class, + () -> api.getTokenForConnection("google-oauth2", null), + "'refresh token' cannot be null!"); + } + + @Test + public void shouldCreateGetTokenForConnectionRequest() throws Exception { + TokenRequest request = api.getTokenForConnection("google-oauth2", "test-refresh-token"); + 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-refresh-token")); + assertThat(body, hasEntry("subject_token_type", "urn:ietf:params:oauth:token-type:refresh_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 shouldCreateGetTokenForConnectionRequestWithLoginHint() throws Exception { + TokenRequest request = api.getTokenForConnection("google-oauth2", "test-refresh-token") + .setLoginHint("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 getTokenForConnectionRequiresClientAuthentication() { + verifyThrows( + IllegalStateException.class, + () -> apiNoClientAuthentication.getTokenForConnection("google-oauth2", "test-refresh-token"), + "A client secret or client assertion signing key is required for this operation"); + } + // Login with Passwordless @Test From 646d18d7c8f96d946037f2f17071b2b43690c945 Mon Sep 17 00:00:00 2001 From: Kailash B Date: Thu, 9 Jul 2026 16:20:46 +0530 Subject: [PATCH 2/3] chore: apply spotless formatting --- .../java/com/auth0/client/auth/AuthAPI.java | 7 +++---- .../java/com/auth0/client/auth/AuthAPITest.java | 17 +++++++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/auth0/client/auth/AuthAPI.java b/src/main/java/com/auth0/client/auth/AuthAPI.java index e58e4caec..a74a2f07c 100644 --- a/src/main/java/com/auth0/client/auth/AuthAPI.java +++ b/src/main/java/com/auth0/client/auth/AuthAPI.java @@ -61,11 +61,10 @@ public class AuthAPI { private static final String KEY_SUBJECT_TOKEN_TYPE = "subject_token_type"; private static final String KEY_REQUESTED_TOKEN_TYPE = "requested_token_type"; private static final String GRANT_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN = - "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token"; - private static final String SUBJECT_TOKEN_TYPE_REFRESH_TOKEN = - "urn:ietf:params:oauth:token-type:refresh_token"; + "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token"; + private static final String SUBJECT_TOKEN_TYPE_REFRESH_TOKEN = "urn:ietf:params:oauth:token-type:refresh_token"; private static final String REQUESTED_TOKEN_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN = - "http://auth0.com/oauth/token-type/federated-connection-access-token"; + "http://auth0.com/oauth/token-type/federated-connection-access-token"; private static final String PATH_OAUTH = "oauth"; private static final String PATH_TOKEN = "token"; private static final String PATH_DBCONNECTIONS = "dbconnections"; diff --git a/src/test/java/com/auth0/client/auth/AuthAPITest.java b/src/test/java/com/auth0/client/auth/AuthAPITest.java index 00023f674..c4682bb0a 100644 --- a/src/test/java/com/auth0/client/auth/AuthAPITest.java +++ b/src/test/java/com/auth0/client/auth/AuthAPITest.java @@ -1066,14 +1066,19 @@ public void shouldCreateGetTokenForConnectionRequest() throws Exception { 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( + "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-refresh-token")); assertThat(body, hasEntry("subject_token_type", "urn:ietf:params:oauth:token-type:refresh_token")); - assertThat(body, hasEntry("requested_token_type", - "http://auth0.com/oauth/token-type/federated-connection-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"))); @@ -1083,8 +1088,8 @@ public void shouldCreateGetTokenForConnectionRequest() throws Exception { @Test public void shouldCreateGetTokenForConnectionRequestWithLoginHint() throws Exception { - TokenRequest request = api.getTokenForConnection("google-oauth2", "test-refresh-token") - .setLoginHint("google-user-id"); + TokenRequest request = + api.getTokenForConnection("google-oauth2", "test-refresh-token").setLoginHint("google-user-id"); assertThat(request, is(notNullValue())); server.jsonResponse(AUTH_TOKENS, 200); From f3293b211f01d308d19db6191148990307658a0a Mon Sep 17 00:00:00 2001 From: Kailash B Date: Fri, 10 Jul 2026 16:42:55 +0530 Subject: [PATCH 3/3] chore: Adds LoginHint as an optional parameter --- .../java/com/auth0/client/auth/AuthAPI.java | 25 +++++++++++-------- src/main/java/com/auth0/net/TokenRequest.java | 13 ---------- .../com/auth0/client/auth/AuthAPITest.java | 11 ++++---- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/auth0/client/auth/AuthAPI.java b/src/main/java/com/auth0/client/auth/AuthAPI.java index a74a2f07c..342e70ca6 100644 --- a/src/main/java/com/auth0/client/auth/AuthAPI.java +++ b/src/main/java/com/auth0/client/auth/AuthAPI.java @@ -60,11 +60,7 @@ public class AuthAPI { private static final String KEY_SUBJECT_TOKEN = "subject_token"; private static final String KEY_SUBJECT_TOKEN_TYPE = "subject_token_type"; private static final String KEY_REQUESTED_TOKEN_TYPE = "requested_token_type"; - private static final String GRANT_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN = - "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token"; - private static final String SUBJECT_TOKEN_TYPE_REFRESH_TOKEN = "urn:ietf:params:oauth:token-type:refresh_token"; - private static final String REQUESTED_TOKEN_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN = - "http://auth0.com/oauth/token-type/federated-connection-access-token"; + private static final String KEY_LOGIN_HINT = "login_hint"; private static final String PATH_OAUTH = "oauth"; private static final String PATH_TOKEN = "token"; private static final String PATH_DBCONNECTIONS = "dbconnections"; @@ -855,8 +851,7 @@ public TokenRequest exchangeToken(String subjectToken, String subjectTokenType) *
      * {@code
      * try {
-     *      TokenHolder result = authAPI.getTokenForConnection("google-oauth2", refreshToken)
-     *          .setLoginHint("google-user-id")
+     *      TokenHolder result = authAPI.getTokenForConnection("google-oauth2", refreshToken, "google-user-id")
      *          .execute()
      *          .getBody();
      *      String federatedAccessToken = result.getAccessToken();
@@ -870,19 +865,27 @@ public TokenRequest exchangeToken(String subjectToken, String subjectTokenType)
      * @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 getTokenForConnection(String connection, String refreshToken) {
+    public TokenRequest getTokenForConnection(String connection, String refreshToken, String loginHint) {
         Asserts.assertNotNull(connection, "connection");
         Asserts.assertNotNull(refreshToken, "refresh token");
 
         TokenRequest request = new TokenRequest(client, getTokenUrl());
         request.addParameter(KEY_CLIENT_ID, clientId);
-        request.addParameter(KEY_GRANT_TYPE, GRANT_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN);
+        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, SUBJECT_TOKEN_TYPE_REFRESH_TOKEN);
-        request.addParameter(KEY_REQUESTED_TOKEN_TYPE, REQUESTED_TOKEN_TYPE_FEDERATED_CONNECTION_ACCESS_TOKEN);
+        request.addParameter(KEY_SUBJECT_TOKEN_TYPE, "urn:ietf:params:oauth:token-type:refresh_token");
+        request.addParameter(
+                KEY_REQUESTED_TOKEN_TYPE, "http://auth0.com/oauth/token-type/federated-connection-access-token");
         request.addParameter(KEY_CONNECTION, connection);
+        if (loginHint != null) {
+            request.addParameter(KEY_LOGIN_HINT, loginHint);
+        }
         addClientAuthentication(request, true);
         return request;
     }
diff --git a/src/main/java/com/auth0/net/TokenRequest.java b/src/main/java/com/auth0/net/TokenRequest.java
index ca94f4266..800c5f4a3 100644
--- a/src/main/java/com/auth0/net/TokenRequest.java
+++ b/src/main/java/com/auth0/net/TokenRequest.java
@@ -46,17 +46,4 @@ public TokenRequest setScope(String scope) {
         super.addParameter("scope", scope);
         return this;
     }
-
-    /**
-     * Setter for the login hint value to request. For the Token Vault federated connection
-     * token exchange, this is 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}).
-     *
-     * @param loginHint the login hint to request
-     * @return this request instance.
-     */
-    public TokenRequest setLoginHint(String loginHint) {
-        super.addParameter("login_hint", loginHint);
-        return this;
-    }
 }
diff --git a/src/test/java/com/auth0/client/auth/AuthAPITest.java b/src/test/java/com/auth0/client/auth/AuthAPITest.java
index c4682bb0a..7278a80ef 100644
--- a/src/test/java/com/auth0/client/auth/AuthAPITest.java
+++ b/src/test/java/com/auth0/client/auth/AuthAPITest.java
@@ -1041,7 +1041,7 @@ public void shouldCreateTokenExchangeRequestWithClientAssertion() throws Excepti
     public void shouldThrowOnGetTokenForConnectionWithNullConnection() {
         verifyThrows(
                 IllegalArgumentException.class,
-                () -> api.getTokenForConnection(null, "test-refresh-token"),
+                () -> api.getTokenForConnection(null, "test-refresh-token", null),
                 "'connection' cannot be null!");
     }
 
@@ -1049,13 +1049,13 @@ public void shouldThrowOnGetTokenForConnectionWithNullConnection() {
     public void shouldThrowOnGetTokenForConnectionWithNullRefreshToken() {
         verifyThrows(
                 IllegalArgumentException.class,
-                () -> api.getTokenForConnection("google-oauth2", null),
+                () -> api.getTokenForConnection("google-oauth2", null, null),
                 "'refresh token' cannot be null!");
     }
 
     @Test
     public void shouldCreateGetTokenForConnectionRequest() throws Exception {
-        TokenRequest request = api.getTokenForConnection("google-oauth2", "test-refresh-token");
+        TokenRequest request = api.getTokenForConnection("google-oauth2", "test-refresh-token", null);
         assertThat(request, is(notNullValue()));
 
         server.jsonResponse(AUTH_TOKENS, 200);
@@ -1088,8 +1088,7 @@ public void shouldCreateGetTokenForConnectionRequest() throws Exception {
 
     @Test
     public void shouldCreateGetTokenForConnectionRequestWithLoginHint() throws Exception {
-        TokenRequest request =
-                api.getTokenForConnection("google-oauth2", "test-refresh-token").setLoginHint("google-user-id");
+        TokenRequest request = api.getTokenForConnection("google-oauth2", "test-refresh-token", "google-user-id");
         assertThat(request, is(notNullValue()));
 
         server.jsonResponse(AUTH_TOKENS, 200);
@@ -1108,7 +1107,7 @@ public void shouldCreateGetTokenForConnectionRequestWithLoginHint() throws Excep
     public void getTokenForConnectionRequiresClientAuthentication() {
         verifyThrows(
                 IllegalStateException.class,
-                () -> apiNoClientAuthentication.getTokenForConnection("google-oauth2", "test-refresh-token"),
+                () -> apiNoClientAuthentication.getTokenForConnection("google-oauth2", "test-refresh-token", null),
                 "A client secret or client assertion signing key is required for this operation");
     }