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..c36975dfb 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,97 @@ 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..d78699384 100644
--- a/src/test/java/com/auth0/client/auth/AuthAPITest.java
+++ b/src/test/java/com/auth0/client/auth/AuthAPITest.java
@@ -1041,21 +1041,110 @@ 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 +1176,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 +1213,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 +1223,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");
}