diff --git a/EXAMPLES.md b/EXAMPLES.md index c2645a8..e34cb1c 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -282,6 +282,58 @@ Tokens tokens = controller.loginWithCustomTokenExchange("EXTERNAL-SUBJECT-TOKEN" .execute(); ``` +## Token Vault (Federated Connection Access Tokens) + +[Token Vault](https://auth0.com/docs/secure/tokens/token-vault) exchanges an Auth0 token for an external identity provider's access token (for example Google, GitHub, or Slack), so your application can call that provider's API on the user's behalf. The returned access token is the external provider's token — opaque to your application — and the grant returns no ID token and no refresh token. + +The library remains stateless: your application owns storage of the subject token, caching of the resulting connection access token, and any concurrency control. + +The subject token can be an Auth0 refresh token or access token. Use the typed factory method that matches what you hold; a generic overload accepts a caller-supplied subject-token-type for other cases. + +```java +// Exchange a refresh token for the connection's access token. +Tokens tokens = controller.getTokenForConnectionWithRefreshToken("google-oauth2", "YOUR-REFRESH-TOKEN") + .execute(); + +// Or exchange an access token. +Tokens tokens = controller.getTokenForConnectionWithAccessToken("google-oauth2", "YOUR-ACCESS-TOKEN") + .execute(); + +// The access token is the external provider's token; the ID and refresh tokens are null. +String connectionAccessToken = tokens.getAccessToken(); +``` + +Optionally set a `login_hint` — the user's ID within the identity provider (for example, the Google user ID when the connection is `google-oauth2`): + +```java +Tokens tokens = controller.getTokenForConnectionWithRefreshToken("google-oauth2", "YOUR-REFRESH-TOKEN") + .withLoginHint("google-user-id") + .execute(); +``` + +For subject-token-types other than the refresh-token and access-token variants, use the generic overload and pass the subject-token-type URN explicitly: + +```java +Tokens tokens = controller.getTokenForConnection("google-oauth2", "YOUR-SUBJECT-TOKEN", "urn:ietf:params:oauth:token-type:refresh_token") + .execute(); +``` + +### Using Token Vault with Multiple Custom Domains + +A connection exchange is bound to the domain the subject token was issued for at login. When using a `DomainResolver`, pass that domain explicitly — supply the value stored from `Tokens.getDomain()` at login. This is required because a connection exchange can occur outside of an HTTP request: + +```java +Tokens tokens = controller.getTokenForConnectionWithRefreshToken("google-oauth2", "YOUR-REFRESH-TOKEN", "acme.auth0.com") + .execute(); +``` + +Alternatively, pass the `HttpServletRequest` to let the resolver derive the domain. Note that if the resolver resolves the request to a different domain than the one the subject token was issued for, Auth0 will reject the grant: + +```java +Tokens tokens = controller.getTokenForConnectionWithRefreshToken("google-oauth2", "YOUR-REFRESH-TOKEN", request) + .execute(); +``` + ## Client-Initiated Backchannel Authentication (CIBA) [CIBA](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow) is a decoupled flow: the application initiates authentication and the user approves it out-of-band on a separate device (e.g., a push notification to their phone). It is a **two-step** flow: diff --git a/README.md b/README.md index 9733e47..f0a0d30 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ Beyond the interactive login flow above, the `AuthenticationController` supports - **Refresh Token Grant (MRRT)** — exchange a refresh token for fresh tokens, including Multi-Resource Refresh Token flows that target multiple APIs. See [EXAMPLES.md](./EXAMPLES.md#refresh-token-grant-mrrt). - **Custom Token Exchange (CTE)** — exchange an external `subject_token` for Auth0 tokens (RFC 8693). See [EXAMPLES.md](./EXAMPLES.md#custom-token-exchange-cte). - **Client-Initiated Backchannel Authentication (CIBA)** — a decoupled flow where the user approves authentication out-of-band on a separate device. See [EXAMPLES.md](./EXAMPLES.md#client-initiated-backchannel-authentication-ciba). +- **Token Vault (Federated Connection Access Tokens)** — exchange an Auth0 token for an external identity provider's access token to call that provider's API on the user's behalf. See [EXAMPLES.md](./EXAMPLES.md#token-vault-federated-connection-access-tokens). ## API Reference diff --git a/src/main/java/com/auth0/AuthenticationController.java b/src/main/java/com/auth0/AuthenticationController.java index 6ca85aa..deadad6 100644 --- a/src/main/java/com/auth0/AuthenticationController.java +++ b/src/main/java/com/auth0/AuthenticationController.java @@ -615,4 +615,185 @@ public TokenExchangeRequest loginWithCustomTokenExchange(String subjectToken, St return requestProcessor.buildTokenExchangeRequest(subjectToken, subjectTokenType, true); } + /** + * Builds a request to exchange an Auth0 refresh token for an external identity provider's + * access token via Token Vault, + * using the statically configured domain. + * + *
This overload is only valid when the controller was configured with a fixed domain. When a + * {@code DomainResolver} is in use, call + * {@link #getTokenForConnectionWithRefreshToken(String, String, String)} with the domain.
+ * + * @param connection the federated connection name (e.g. {@code google-oauth2}). + * @param refreshToken the Auth0 refresh token to exchange. + * @return a {@link ConnectionTokenRequest} to configure and execute. + * @throws IllegalStateException if the controller was configured with a {@code DomainResolver}. + */ + public ConnectionTokenRequest getTokenForConnectionWithRefreshToken(String connection, String refreshToken) { + Validate.notNull(connection, "connection must not be null"); + Validate.notNull(refreshToken, "refreshToken must not be null"); + return requestProcessor.buildConnectionTokenRequest(connection, refreshToken, "urn:ietf:params:oauth:token-type:refresh_token"); + } + + /** + * Builds a Token Vault request exchanging an Auth0 refresh token against the given domain. + * + *A connection exchange is bound to the domain the subject token was issued for at login; + * supply the domain stored from {@link Tokens#getDomain()} at login. This overload is required + * in Multiple Custom Domains (MCD) setups, and works outside of an HTTP request.
+ * + * @param connection the federated connection name. + * @param refreshToken the Auth0 refresh token to exchange. + * @param domain the Auth0 domain to target. + * @return a {@link ConnectionTokenRequest} to configure and execute. + */ + public ConnectionTokenRequest getTokenForConnectionWithRefreshToken(String connection, String refreshToken, String domain) { + Validate.notNull(connection, "connection must not be null"); + Validate.notNull(refreshToken, "refreshToken must not be null"); + Validate.notNull(domain, "domain must not be null"); + return requestProcessor.buildConnectionTokenRequest(connection, refreshToken, "urn:ietf:params:oauth:token-type:refresh_token", domain); + } + + /** + * Builds a Token Vault request exchanging an Auth0 refresh token, resolving the domain from the + * given request via the configured domain or {@code DomainResolver}. + * + *Note: a connection exchange is bound to the domain the subject token was + * issued for at login; if the resolver resolves this request to a different domain, Auth0 will + * reject the grant. In MCD setups prefer + * {@link #getTokenForConnectionWithRefreshToken(String, String, String)} with the domain stored + * from {@link Tokens#getDomain()} at login.
+ * + * @param connection the federated connection name. + * @param refreshToken the Auth0 refresh token to exchange. + * @param request the current HTTP request, used to resolve the domain. + * @return a {@link ConnectionTokenRequest} to configure and execute. + */ + public ConnectionTokenRequest getTokenForConnectionWithRefreshToken(String connection, String refreshToken, HttpServletRequest request) { + Validate.notNull(connection, "connection must not be null"); + Validate.notNull(refreshToken, "refreshToken must not be null"); + Validate.notNull(request, "request must not be null"); + return requestProcessor.buildConnectionTokenRequest(connection, refreshToken, "urn:ietf:params:oauth:token-type:refresh_token", request); + } + + /** + * Builds a request to exchange an Auth0 access token for an external identity provider's access + * token via Token Vault, using + * the statically configured domain. + * + *This overload is only valid when the controller was configured with a fixed domain. When a + * {@code DomainResolver} is in use, call + * {@link #getTokenForConnectionWithAccessToken(String, String, String)} with the domain.
+ * + * @param connection the federated connection name (e.g. {@code google-oauth2}). + * @param accessToken the Auth0 access token to exchange. + * @return a {@link ConnectionTokenRequest} to configure and execute. + * @throws IllegalStateException if the controller was configured with a {@code DomainResolver}. + */ + public ConnectionTokenRequest getTokenForConnectionWithAccessToken(String connection, String accessToken) { + Validate.notNull(connection, "connection must not be null"); + Validate.notNull(accessToken, "accessToken must not be null"); + return requestProcessor.buildConnectionTokenRequest(connection, accessToken, "urn:ietf:params:oauth:token-type:access_token"); + } + + /** + * Builds a Token Vault request exchanging an Auth0 access token against the given domain. + * + *A connection exchange is bound to the domain the subject token was issued for at login; + * supply the domain stored from {@link Tokens#getDomain()} at login. This overload is required + * in Multiple Custom Domains (MCD) setups.
+ * + * @param connection the federated connection name. + * @param accessToken the Auth0 access token to exchange. + * @param domain the Auth0 domain to target. + * @return a {@link ConnectionTokenRequest} to configure and execute. + */ + public ConnectionTokenRequest getTokenForConnectionWithAccessToken(String connection, String accessToken, String domain) { + Validate.notNull(connection, "connection must not be null"); + Validate.notNull(accessToken, "accessToken must not be null"); + Validate.notNull(domain, "domain must not be null"); + return requestProcessor.buildConnectionTokenRequest(connection, accessToken, "urn:ietf:params:oauth:token-type:access_token", domain); + } + + /** + * Builds a Token Vault request exchanging an Auth0 access token, resolving the domain from the + * given request via the configured domain or {@code DomainResolver}. + * + *Note: a connection exchange is bound to the domain the subject token was + * issued for at login; if the resolver resolves this request to a different domain, Auth0 will + * reject the grant. In MCD setups prefer + * {@link #getTokenForConnectionWithAccessToken(String, String, String)} with the domain stored + * from {@link Tokens#getDomain()} at login.
+ * + * @param connection the federated connection name. + * @param accessToken the Auth0 access token to exchange. + * @param request the current HTTP request, used to resolve the domain. + * @return a {@link ConnectionTokenRequest} to configure and execute. + */ + public ConnectionTokenRequest getTokenForConnectionWithAccessToken(String connection, String accessToken, HttpServletRequest request) { + Validate.notNull(connection, "connection must not be null"); + Validate.notNull(accessToken, "accessToken must not be null"); + Validate.notNull(request, "request must not be null"); + return requestProcessor.buildConnectionTokenRequest(connection, accessToken, "urn:ietf:params:oauth:token-type:access_token", request); + } + + /** + * Builds a Token Vault request with a caller-supplied subject-token-type, using the statically + * configured domain. This is the generic escape hatch for subject-token-types other than the + * refresh-token and access-token variants covered by the typed methods. + * + *This overload is only valid when the controller was configured with a fixed domain. When a + * {@code DomainResolver} is in use, call + * {@link #getTokenForConnection(String, String, String, String)} with the domain.
+ * + * @param connection the federated connection name (e.g. {@code google-oauth2}). + * @param subjectToken the Auth0 token to exchange. + * @param subjectTokenType the subject-token-type URN describing {@code subjectToken}. + * @return a {@link ConnectionTokenRequest} to configure and execute. + * @throws IllegalStateException if the controller was configured with a {@code DomainResolver}. + */ + public ConnectionTokenRequest getTokenForConnection(String connection, String subjectToken, String subjectTokenType) { + Validate.notNull(connection, "connection must not be null"); + Validate.notNull(subjectToken, "subjectToken must not be null"); + Validate.notNull(subjectTokenType, "subjectTokenType must not be null"); + return requestProcessor.buildConnectionTokenRequest(connection, subjectToken, subjectTokenType); + } + + /** + * Builds a Token Vault request with a caller-supplied subject-token-type against the given + * domain. See {@link #getTokenForConnection(String, String, String)} for details. + * + * @param connection the federated connection name. + * @param subjectToken the Auth0 token to exchange. + * @param subjectTokenType the subject-token-type URN describing {@code subjectToken}. + * @param domain the Auth0 domain to target. + * @return a {@link ConnectionTokenRequest} to configure and execute. + */ + public ConnectionTokenRequest getTokenForConnection(String connection, String subjectToken, String subjectTokenType, String domain) { + Validate.notNull(connection, "connection must not be null"); + Validate.notNull(subjectToken, "subjectToken must not be null"); + Validate.notNull(subjectTokenType, "subjectTokenType must not be null"); + Validate.notNull(domain, "domain must not be null"); + return requestProcessor.buildConnectionTokenRequest(connection, subjectToken, subjectTokenType, domain); + } + + /** + * Builds a Token Vault request with a caller-supplied subject-token-type, resolving the domain + * from the given request. See {@link #getTokenForConnection(String, String, String)} for + * details and the domain-binding caveat. + * + * @param connection the federated connection name. + * @param subjectToken the Auth0 token to exchange. + * @param subjectTokenType the subject-token-type URN describing {@code subjectToken}. + * @param request the current HTTP request, used to resolve the domain. + * @return a {@link ConnectionTokenRequest} to configure and execute. + */ + public ConnectionTokenRequest getTokenForConnection(String connection, String subjectToken, String subjectTokenType, HttpServletRequest request) { + Validate.notNull(connection, "connection must not be null"); + Validate.notNull(subjectToken, "subjectToken must not be null"); + Validate.notNull(subjectTokenType, "subjectTokenType must not be null"); + Validate.notNull(request, "request must not be null"); + return requestProcessor.buildConnectionTokenRequest(connection, subjectToken, subjectTokenType, request); + } + } diff --git a/src/main/java/com/auth0/ConnectionTokenRequest.java b/src/main/java/com/auth0/ConnectionTokenRequest.java new file mode 100644 index 0000000..32ec9e2 --- /dev/null +++ b/src/main/java/com/auth0/ConnectionTokenRequest.java @@ -0,0 +1,77 @@ +package com.auth0; + +import com.auth0.client.auth.AuthAPI; +import com.auth0.exception.Auth0Exception; +import com.auth0.json.auth.TokenHolder; +import com.auth0.net.TokenRequest; + +/** + * Class to exchange an Auth0 token for an external identity provider's access token via + * Token Vault (the + * federated-connection access-token grant). The returned access token is the external provider's + * token (for example Google, GitHub, or Slack), opaque to this application, suitable for calling + * that provider's API on the user's behalf. + *+ * The subject token may be an Auth0 refresh token or access token; the subject-token-type is fixed + * by the factory method used to create this request. The federated-connection grant returns an + * access token, {@code scope}, {@code expires_in}, and {@code token_type} — no ID token and no + * refresh token. + *
+ * The library remains stateless: the application owns storage of the subject token, caching of the + * resulting connection access token, and any concurrency control. + *
+ * Obtain an instance via one of the {@code AuthenticationController} factory methods:
+ * {@link AuthenticationController#getTokenForConnectionWithRefreshToken(String, String)},
+ * {@link AuthenticationController#getTokenForConnectionWithAccessToken(String, String)}, or
+ * {@link AuthenticationController#getTokenForConnection(String, String, String)} (and their
+ * domain / request overloads).
+ */
+@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "unused"})
+public class ConnectionTokenRequest {
+
+ private final AuthAPI client;
+ private final String connection;
+ private final String subjectToken;
+ private final String subjectTokenType;
+ private final String domain;
+ private final String issuer;
+ private String loginHint;
+
+ ConnectionTokenRequest(AuthAPI client, String connection, String subjectToken,
+ String subjectTokenType, String domain, String issuer) {
+ this.client = client;
+ this.connection = connection;
+ this.subjectToken = subjectToken;
+ this.subjectTokenType = subjectTokenType;
+ this.domain = domain;
+ this.issuer = issuer;
+ }
+
+ /**
+ * Sets the {@code login_hint} — 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}).
+ * When not set, no {@code login_hint} is sent.
+ *
+ * @param loginHint the provider-side identity provider user ID.
+ * @return this request instance for fluent chaining.
+ */
+ public ConnectionTokenRequest withLoginHint(String loginHint) {
+ this.loginHint = loginHint;
+ return this;
+ }
+
+ /**
+ * Executes the federated-connection token exchange against Auth0 and returns the resulting
+ * tokens. The returned {@link Tokens#getAccessToken()} is the external provider's access token;
+ * {@link Tokens#getIdToken()} and {@link Tokens#getRefreshToken()} are null.
+ *
+ * @return the {@link Tokens} obtained from the grant.
+ * @throws Auth0Exception if the request to the Auth0 server failed.
+ */
+ public Tokens execute() throws Auth0Exception {
+ TokenRequest request = client.getTokenForConnection(connection, subjectToken, subjectTokenType, loginHint);
+ TokenHolder holder = request.execute().getBody();
+ return new Tokens(holder.getAccessToken(), null, null,
+ holder.getTokenType(), holder.getExpiresIn(), holder.getScope(), domain, issuer);
+ }
+}
diff --git a/src/main/java/com/auth0/RequestProcessor.java b/src/main/java/com/auth0/RequestProcessor.java
index 8dcb591..cbda1f1 100644
--- a/src/main/java/com/auth0/RequestProcessor.java
+++ b/src/main/java/com/auth0/RequestProcessor.java
@@ -217,6 +217,59 @@ RenewAuthRequest buildRenewAuthRequest(String refreshToken, HttpServletRequest r
return buildRenewAuthRequest(refreshToken, domainProvider.getDomain(request));
}
+ /**
+ * Builds a {@link ConnectionTokenRequest} to exchange an Auth0 subject token for an external
+ * identity provider's access token (Token Vault) against the given domain. The domain is
+ * supplied explicitly because a connection exchange can occur outside of an HTTP request, where
+ * the {@link DomainProvider} cannot resolve it.
+ *
+ * @param connection the federated connection name (e.g. {@code google-oauth2}).
+ * @param subjectToken the Auth0 token to exchange.
+ * @param subjectTokenType the subject-token-type URN describing {@code subjectToken}.
+ * @param domain the Auth0 domain to target.
+ * @return a {@link ConnectionTokenRequest} ready to configure and execute.
+ */
+ ConnectionTokenRequest buildConnectionTokenRequest(
+ String connection, String subjectToken, String subjectTokenType, String domain) {
+ AuthAPI client = createClientForDomain(domain);
+ String issuer = constructIssuer(domain);
+ return new ConnectionTokenRequest(client, connection, subjectToken, subjectTokenType, domain, issuer);
+ }
+
+ /**
+ * Builds a {@link ConnectionTokenRequest} using the statically configured domain. Only valid
+ * when the controller was configured with a fixed domain; when a {@link DomainResolver} is in
+ * use the domain must be supplied explicitly.
+ *
+ * @param connection the federated connection name.
+ * @param subjectToken the Auth0 token to exchange.
+ * @param subjectTokenType the subject-token-type URN describing {@code subjectToken}.
+ * @return a {@link ConnectionTokenRequest} ready to configure and execute.
+ * @throws IllegalStateException if the controller was configured with a {@link DomainResolver}.
+ */
+ ConnectionTokenRequest buildConnectionTokenRequest(
+ String connection, String subjectToken, String subjectTokenType) {
+ if (!(domainProvider instanceof StaticDomainProvider)) {
+ throw new IllegalStateException("A domain is required when using a DomainResolver; call the getTokenForConnection overload that accepts a domain.");
+ }
+ return buildConnectionTokenRequest(connection, subjectToken, subjectTokenType, domainProvider.getDomain(null));
+ }
+
+ /**
+ * Builds a {@link ConnectionTokenRequest} resolving the domain from the given request via the
+ * configured {@link DomainProvider}. Works for both a fixed domain and a {@link DomainResolver}.
+ *
+ * @param connection the federated connection name.
+ * @param subjectToken the Auth0 token to exchange.
+ * @param subjectTokenType the subject-token-type URN describing {@code subjectToken}.
+ * @param request the current HTTP request, used to resolve the domain.
+ * @return a {@link ConnectionTokenRequest} ready to configure and execute.
+ */
+ ConnectionTokenRequest buildConnectionTokenRequest(
+ String connection, String subjectToken, String subjectTokenType, HttpServletRequest request) {
+ return buildConnectionTokenRequest(connection, subjectToken, subjectTokenType, domainProvider.getDomain(request));
+ }
+
/**
* Builds a {@link TokenExchangeRequest} to exchange an external {@code subject_token} for Auth0
* tokens against the given domain. The domain is supplied explicitly because a token exchange
diff --git a/src/test/java/com/auth0/AuthenticationControllerTest.java b/src/test/java/com/auth0/AuthenticationControllerTest.java
index fed0d2d..ab1ea0e 100644
--- a/src/test/java/com/auth0/AuthenticationControllerTest.java
+++ b/src/test/java/com/auth0/AuthenticationControllerTest.java
@@ -431,6 +431,188 @@ public void shouldThrowExceptionWhenLoginWithCustomTokenExchangeSubjectTokenIsNu
assertThat(exception.getMessage(), is("subjectToken must not be null"));
}
+ // --- getTokenForConnection (Token Vault) Tests ---
+
+ private static final String REFRESH_SUBJECT_TYPE = "urn:ietf:params:oauth:token-type:refresh_token";
+ private static final String ACCESS_SUBJECT_TYPE = "urn:ietf:params:oauth:token-type:access_token";
+
+ @Test
+ public void shouldGetTokenForConnectionWithRefreshTokenStaticDomain() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ ConnectionTokenRequest mockRequest = mock(ConnectionTokenRequest.class);
+ when(mockRequestProcessor.buildConnectionTokenRequest("google-oauth2", "refreshToken", REFRESH_SUBJECT_TYPE))
+ .thenReturn(mockRequest);
+
+ ConnectionTokenRequest result = controller.getTokenForConnectionWithRefreshToken("google-oauth2", "refreshToken");
+
+ assertThat(result, is(mockRequest));
+ verify(mockRequestProcessor).buildConnectionTokenRequest("google-oauth2", "refreshToken", REFRESH_SUBJECT_TYPE);
+ }
+
+ @Test
+ public void shouldGetTokenForConnectionWithRefreshTokenExplicitDomain() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ ConnectionTokenRequest mockRequest = mock(ConnectionTokenRequest.class);
+ when(mockRequestProcessor.buildConnectionTokenRequest("google-oauth2", "refreshToken", REFRESH_SUBJECT_TYPE, DOMAIN))
+ .thenReturn(mockRequest);
+
+ ConnectionTokenRequest result = controller.getTokenForConnectionWithRefreshToken("google-oauth2", "refreshToken", DOMAIN);
+
+ assertThat(result, is(mockRequest));
+ verify(mockRequestProcessor).buildConnectionTokenRequest("google-oauth2", "refreshToken", REFRESH_SUBJECT_TYPE, DOMAIN);
+ }
+
+ @Test
+ public void shouldGetTokenForConnectionWithRefreshTokenFromRequest() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ ConnectionTokenRequest mockRequest = mock(ConnectionTokenRequest.class);
+ when(mockRequestProcessor.buildConnectionTokenRequest("google-oauth2", "refreshToken", REFRESH_SUBJECT_TYPE, request))
+ .thenReturn(mockRequest);
+
+ ConnectionTokenRequest result = controller.getTokenForConnectionWithRefreshToken("google-oauth2", "refreshToken", request);
+
+ assertThat(result, is(mockRequest));
+ verify(mockRequestProcessor).buildConnectionTokenRequest("google-oauth2", "refreshToken", REFRESH_SUBJECT_TYPE, request);
+ }
+
+ @Test
+ public void shouldGetTokenForConnectionWithAccessTokenStaticDomain() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ ConnectionTokenRequest mockRequest = mock(ConnectionTokenRequest.class);
+ when(mockRequestProcessor.buildConnectionTokenRequest("google-oauth2", "accessToken", ACCESS_SUBJECT_TYPE))
+ .thenReturn(mockRequest);
+
+ ConnectionTokenRequest result = controller.getTokenForConnectionWithAccessToken("google-oauth2", "accessToken");
+
+ assertThat(result, is(mockRequest));
+ verify(mockRequestProcessor).buildConnectionTokenRequest("google-oauth2", "accessToken", ACCESS_SUBJECT_TYPE);
+ }
+
+ @Test
+ public void shouldGetTokenForConnectionWithAccessTokenExplicitDomain() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ ConnectionTokenRequest mockRequest = mock(ConnectionTokenRequest.class);
+ when(mockRequestProcessor.buildConnectionTokenRequest("google-oauth2", "accessToken", ACCESS_SUBJECT_TYPE, DOMAIN))
+ .thenReturn(mockRequest);
+
+ ConnectionTokenRequest result = controller.getTokenForConnectionWithAccessToken("google-oauth2", "accessToken", DOMAIN);
+
+ assertThat(result, is(mockRequest));
+ verify(mockRequestProcessor).buildConnectionTokenRequest("google-oauth2", "accessToken", ACCESS_SUBJECT_TYPE, DOMAIN);
+ }
+
+ @Test
+ public void shouldGetTokenForConnectionWithAccessTokenFromRequest() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ ConnectionTokenRequest mockRequest = mock(ConnectionTokenRequest.class);
+ when(mockRequestProcessor.buildConnectionTokenRequest("google-oauth2", "accessToken", ACCESS_SUBJECT_TYPE, request))
+ .thenReturn(mockRequest);
+
+ ConnectionTokenRequest result = controller.getTokenForConnectionWithAccessToken("google-oauth2", "accessToken", request);
+
+ assertThat(result, is(mockRequest));
+ verify(mockRequestProcessor).buildConnectionTokenRequest("google-oauth2", "accessToken", ACCESS_SUBJECT_TYPE, request);
+ }
+
+ @Test
+ public void shouldGetTokenForConnectionGenericStaticDomain() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ ConnectionTokenRequest mockRequest = mock(ConnectionTokenRequest.class);
+ when(mockRequestProcessor.buildConnectionTokenRequest("google-oauth2", "subjectToken", "custom:type"))
+ .thenReturn(mockRequest);
+
+ ConnectionTokenRequest result = controller.getTokenForConnection("google-oauth2", "subjectToken", "custom:type");
+
+ assertThat(result, is(mockRequest));
+ verify(mockRequestProcessor).buildConnectionTokenRequest("google-oauth2", "subjectToken", "custom:type");
+ }
+
+ @Test
+ public void shouldGetTokenForConnectionGenericExplicitDomain() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ ConnectionTokenRequest mockRequest = mock(ConnectionTokenRequest.class);
+ when(mockRequestProcessor.buildConnectionTokenRequest("google-oauth2", "subjectToken", "custom:type", DOMAIN))
+ .thenReturn(mockRequest);
+
+ ConnectionTokenRequest result = controller.getTokenForConnection("google-oauth2", "subjectToken", "custom:type", DOMAIN);
+
+ assertThat(result, is(mockRequest));
+ verify(mockRequestProcessor).buildConnectionTokenRequest("google-oauth2", "subjectToken", "custom:type", DOMAIN);
+ }
+
+ @Test
+ public void shouldGetTokenForConnectionGenericFromRequest() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ ConnectionTokenRequest mockRequest = mock(ConnectionTokenRequest.class);
+ when(mockRequestProcessor.buildConnectionTokenRequest("google-oauth2", "subjectToken", "custom:type", request))
+ .thenReturn(mockRequest);
+
+ ConnectionTokenRequest result = controller.getTokenForConnection("google-oauth2", "subjectToken", "custom:type", request);
+
+ assertThat(result, is(mockRequest));
+ verify(mockRequestProcessor).buildConnectionTokenRequest("google-oauth2", "subjectToken", "custom:type", request);
+ }
+
+ @Test
+ public void shouldThrowWhenConnectionIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.getTokenForConnectionWithRefreshToken(null, "refreshToken"));
+ assertThat(exception.getMessage(), is("connection must not be null"));
+ }
+
+ @Test
+ public void shouldThrowWhenRefreshTokenIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.getTokenForConnectionWithRefreshToken("google-oauth2", null));
+ assertThat(exception.getMessage(), is("refreshToken must not be null"));
+ }
+
+ @Test
+ public void shouldThrowWhenAccessTokenIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.getTokenForConnectionWithAccessToken("google-oauth2", null));
+ assertThat(exception.getMessage(), is("accessToken must not be null"));
+ }
+
+ @Test
+ public void shouldThrowWhenSubjectTokenTypeIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.getTokenForConnection("google-oauth2", "subjectToken", null));
+ assertThat(exception.getMessage(), is("subjectTokenType must not be null"));
+ }
+
+ @Test
+ public void shouldThrowWhenGetTokenForConnectionDomainIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.getTokenForConnectionWithRefreshToken("google-oauth2", "refreshToken", (String) null));
+ assertThat(exception.getMessage(), is("domain must not be null"));
+ }
+
+ @Test
+ public void shouldThrowWhenGetTokenForConnectionRequestIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.getTokenForConnectionWithRefreshToken("google-oauth2", "refreshToken", (HttpServletRequest) null));
+ assertThat(exception.getMessage(), is("request must not be null"));
+ }
+
// --- backChannelAuthorize Tests ---
@Test
diff --git a/src/test/java/com/auth0/ConnectionTokenRequestTest.java b/src/test/java/com/auth0/ConnectionTokenRequestTest.java
new file mode 100644
index 0000000..f1fad30
--- /dev/null
+++ b/src/test/java/com/auth0/ConnectionTokenRequestTest.java
@@ -0,0 +1,107 @@
+package com.auth0;
+
+import com.auth0.client.auth.AuthAPI;
+import com.auth0.exception.Auth0Exception;
+import com.auth0.json.auth.TokenHolder;
+import com.auth0.net.Response;
+import com.auth0.net.TokenRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ConnectionTokenRequestTest {
+
+ private static final String CONNECTION = "google-oauth2";
+ private static final String SUBJECT_TOKEN = "subjectToken";
+ private static final String SUBJECT_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:refresh_token";
+ private static final String DOMAIN = "domain.auth0.com";
+ private static final String ISSUER = "https://domain.auth0.com/";
+
+ @Mock
+ private AuthAPI mockClient;
+ @Mock
+ private TokenRequest mockTokenRequest;
+ @Mock
+ private Response