diff --git a/src/main/java/com/auth0/AuthenticationController.java b/src/main/java/com/auth0/AuthenticationController.java index 62cee5a..e6419f5 100644 --- a/src/main/java/com/auth0/AuthenticationController.java +++ b/src/main/java/com/auth0/AuthenticationController.java @@ -383,4 +383,66 @@ public AuthorizeUrl buildAuthorizeUrl(HttpServletRequest request, HttpServletRes return requestProcessor.buildAuthorizeUrl(request, response, redirectUri, state, nonce); } + /** + * Builds a request to exchange a refresh token for a new set of {@link Tokens}, optionally + * targeting a specific audience and/or scope. This exposes Auth0's refresh-token grant, + * enabling Multi-Resource Refresh Token (MRRT) flows where one refresh token can obtain access + * tokens for multiple APIs. + * + *
The application supplies the {@code domain} it stored from {@link Tokens#getDomain()} at + * login. This is required because a refresh can occur outside of an HTTP request, where the + * domain cannot otherwise be resolved. For applications configured with a fixed domain, the + * {@link AuthenticationController#renewAuth(String)} overload may be used instead.
+ * + * @param refreshToken the refresh token to exchange. + * @param domain the Auth0 domain to target. + * @return a {@link RenewAuthRequest} to configure and execute. + */ + public RenewAuthRequest renewAuth(String refreshToken, String domain) { + Validate.notNull(refreshToken, "refreshToken must not be null"); + Validate.notNull(domain, "domain must not be null"); + return requestProcessor.buildRenewAuthRequest(refreshToken, domain); + } + + /** + * Builds a request to exchange a refresh token for a new set of {@link Tokens} using the + * statically configured domain. See {@link AuthenticationController#renewAuth(String, String)} + * for details. + * + *This overload is only valid when the controller was configured with a fixed domain. When a + * {@code DomainResolver} is in use, call {@link AuthenticationController#renewAuth(String, String)} + * with the domain instead.
+ * + * @param refreshToken the refresh token to exchange. + * @return a {@link RenewAuthRequest} to configure and execute. + * @throws IllegalStateException if the controller was configured with a {@code DomainResolver}. + */ + public RenewAuthRequest renewAuth(String refreshToken) { + Validate.notNull(refreshToken, "refreshToken must not be null"); + return requestProcessor.buildRenewAuthRequest(refreshToken); + } + + /** + * Builds a request to exchange a refresh token for a new set of {@link Tokens}, resolving the + * Auth0 domain from the given request via the configured domain or {@code DomainResolver}. + * See {@link AuthenticationController#renewAuth(String, String)} for details. + * + *This overload works for both a fixed domain and a {@code DomainResolver}, and is convenient + * when refreshing within an active request. Note: a refresh token is bound to + * the domain it was issued for at login; if the resolver resolves the given request to a + * different domain, Auth0 will reject the grant. Use this overload only when the request + * resolves to the same domain as login; otherwise use + * {@link AuthenticationController#renewAuth(String, String)} with the domain stored from + * {@link Tokens#getDomain()} at login.
+ * + * @param refreshToken the refresh token to exchange. + * @param request the current HTTP request, used to resolve the domain. + * @return a {@link RenewAuthRequest} to configure and execute. + */ + public RenewAuthRequest renewAuth(String refreshToken, HttpServletRequest request) { + Validate.notNull(refreshToken, "refreshToken must not be null"); + Validate.notNull(request, "request must not be null"); + return requestProcessor.buildRenewAuthRequest(refreshToken, request); + } + } diff --git a/src/main/java/com/auth0/RenewAuthRequest.java b/src/main/java/com/auth0/RenewAuthRequest.java new file mode 100644 index 0000000..60895bc --- /dev/null +++ b/src/main/java/com/auth0/RenewAuthRequest.java @@ -0,0 +1,88 @@ +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 a refresh token for a new set of {@link Tokens}, optionally targeting a + * specific {@code audience} and/or {@code scope}. This exposes Auth0's refresh-token grant, + * enabling Multi-Resource Refresh Token (MRRT) flows where one refresh token can obtain access + * tokens for multiple APIs. + *+ * The library remains stateless: the application owns storage of the refresh token, caching of + * the resulting access tokens, and any concurrency control around refresh-token rotation. + *
+ * Obtain an instance via {@link AuthenticationController#renewAuth(String, String)}, + * {@link AuthenticationController#renewAuth(String)}, or + * {@link AuthenticationController#renewAuth(String, jakarta.servlet.http.HttpServletRequest)}. + */ +@SuppressWarnings({"UnusedReturnValue", "WeakerAccess", "unused"}) +public class RenewAuthRequest { + + private final AuthAPI client; + private final String refreshToken; + private final String domain; + private final String issuer; + private String audience; + private String scope; + + RenewAuthRequest(AuthAPI client, String refreshToken, String domain, String issuer) { + this.client = client; + this.refreshToken = refreshToken; + this.domain = domain; + this.issuer = issuer; + } + + /** + * Sets the audience to request an access token for. When not set, Auth0 uses the default + * audience configured for the application. + *
+ * Note: if the requested audience is not permitted by the application's MRRT policy, Auth0 + * does not error; it returns a token for the default audience instead. Callers must verify + * the {@code aud} claim of the returned access token. + * + * @param audience the audience (API identifier) to request a token for. + * @return this request instance for fluent chaining. + */ + public RenewAuthRequest withAudience(String audience) { + this.audience = audience; + return this; + } + + /** + * Sets the scope to request for the access token. + * + * @param scope the requested scope. + * @return this request instance for fluent chaining. + */ + public RenewAuthRequest withScope(String scope) { + this.scope = scope; + return this; + } + + /** + * Executes the refresh-token grant against Auth0 and returns the resulting tokens. + *
+ * The refresh-token grant does not return an ID token, so {@link Tokens#getIdToken()} is
+ * typically null. When refresh-token rotation is enabled, the returned
+ * {@link Tokens#getRefreshToken()} is a new refresh token that supersedes the one used here;
+ * the application is responsible for persisting it.
+ *
+ * @return the {@link Tokens} obtained from the grant, including the granted scope.
+ * @throws Auth0Exception if the request to the Auth0 server failed.
+ */
+ public Tokens execute() throws Auth0Exception {
+ TokenRequest request = client.renewAuth(refreshToken);
+ if (audience != null) {
+ request.setAudience(audience);
+ }
+ if (scope != null) {
+ request.setScope(scope);
+ }
+ TokenHolder holder = request.execute().getBody();
+ return new Tokens(holder.getAccessToken(), holder.getIdToken(), holder.getRefreshToken(),
+ 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 c1e9512..dc10e66 100644
--- a/src/main/java/com/auth0/RequestProcessor.java
+++ b/src/main/java/com/auth0/RequestProcessor.java
@@ -170,6 +170,49 @@ AuthAPI createClientForDomain(String domain) {
.build();
}
+ /**
+ * Builds a {@link RenewAuthRequest} to exchange a refresh token for new tokens against the
+ * given domain. The domain is supplied explicitly because a refresh can occur outside of an
+ * HTTP request (e.g. a background refresh), where the {@link DomainProvider} cannot resolve it.
+ *
+ * @param refreshToken the refresh token to exchange.
+ * @param domain the Auth0 domain to target.
+ * @return a {@link RenewAuthRequest} ready to configure and execute.
+ */
+ RenewAuthRequest buildRenewAuthRequest(String refreshToken, String domain) {
+ AuthAPI client = createClientForDomain(domain);
+ String issuer = constructIssuer(domain);
+ return new RenewAuthRequest(client, refreshToken, domain, issuer);
+ }
+
+ /**
+ * Builds a {@link RenewAuthRequest} using the statically configured domain. Only valid when the
+ * controller was configured with a fixed domain; when a {@link DomainResolver} is in use there
+ * is no fixed domain to target and the domain must be supplied explicitly.
+ *
+ * @param refreshToken the refresh token to exchange.
+ * @return a {@link RenewAuthRequest} ready to configure and execute.
+ * @throws IllegalStateException if the controller was configured with a {@link DomainResolver}.
+ */
+ RenewAuthRequest buildRenewAuthRequest(String refreshToken) {
+ if (!(domainProvider instanceof StaticDomainProvider)) {
+ throw new IllegalStateException("A domain is required when using a DomainResolver; call renewAuth(refreshToken, domain).");
+ }
+ return buildRenewAuthRequest(refreshToken, domainProvider.getDomain(null));
+ }
+
+ /**
+ * Builds a {@link RenewAuthRequest} resolving the domain from the given request via the
+ * configured {@link DomainProvider}. Works for both a fixed domain and a {@link DomainResolver}.
+ *
+ * @param refreshToken the refresh token to exchange.
+ * @param request the current HTTP request, used to resolve the domain.
+ * @return a {@link RenewAuthRequest} ready to configure and execute.
+ */
+ RenewAuthRequest buildRenewAuthRequest(String refreshToken, HttpServletRequest request) {
+ return buildRenewAuthRequest(refreshToken, domainProvider.getDomain(request));
+ }
+
private Auth0HttpClient getHttpClient() {
if (this.httpClient == null) {
DefaultHttpClient.Builder httpBuilder = DefaultHttpClient.newBuilder()
@@ -450,7 +493,7 @@ private Tokens exchangeCodeForTokens(String authorizationCode, String redirectUr
.execute()
.getBody();
String originIssuer = constructIssuer(originDomain);
- return new Tokens(holder.getAccessToken(), holder.getIdToken(), holder.getRefreshToken(), holder.getTokenType(), holder.getExpiresIn(), originDomain, originIssuer);
+ return new Tokens(holder.getAccessToken(), holder.getIdToken(), holder.getRefreshToken(), holder.getTokenType(), holder.getExpiresIn(), holder.getScope(), originDomain, originIssuer);
}
/**
@@ -470,15 +513,18 @@ private Tokens mergeTokens(Tokens frontChannelTokens, Tokens codeExchangeTokens)
String accessToken;
String type;
Long expiresIn;
+ String scope;
if (codeExchangeTokens.getAccessToken() != null) {
accessToken = codeExchangeTokens.getAccessToken();
type = codeExchangeTokens.getType();
expiresIn = codeExchangeTokens.getExpiresIn();
+ scope = codeExchangeTokens.getScope();
} else {
accessToken = frontChannelTokens.getAccessToken();
type = frontChannelTokens.getType();
expiresIn = frontChannelTokens.getExpiresIn();
+ scope = frontChannelTokens.getScope();
}
// Prefer ID token from the front-channel
@@ -493,7 +539,7 @@ private Tokens mergeTokens(Tokens frontChannelTokens, Tokens codeExchangeTokens)
String issuer = frontChannelTokens.getIssuer() != null ? frontChannelTokens.getIssuer()
: codeExchangeTokens.getIssuer();
- return new Tokens(accessToken, idToken, refreshToken, type, expiresIn, domain, issuer);
+ return new Tokens(accessToken, idToken, refreshToken, type, expiresIn, scope, domain, issuer);
}
private String constructIssuer(String domain) {
diff --git a/src/main/java/com/auth0/Tokens.java b/src/main/java/com/auth0/Tokens.java
index cd3951d..fa02682 100644
--- a/src/main/java/com/auth0/Tokens.java
+++ b/src/main/java/com/auth0/Tokens.java
@@ -22,6 +22,7 @@ public class Tokens implements Serializable {
private final String refreshToken;
private final String type;
private final Long expiresIn;
+ private final String scope;
private final String domain;
private final String issuer;
@@ -49,11 +50,29 @@ public Tokens(String accessToken, String idToken, String refreshToken, String ty
* @param issuer the issuer URL from the ID token
*/
public Tokens(String accessToken, String idToken, String refreshToken, String type, Long expiresIn, String domain, String issuer) {
+ this(accessToken, idToken, refreshToken, type, expiresIn, null, domain, issuer);
+ }
+
+ /**
+ * Full constructor including the granted scope.
+ *
+ * @param accessToken access token for Auth0 API
+ * @param idToken identity token with user information
+ * @param refreshToken refresh token that can be used to request new tokens
+ * without signing in again
+ * @param type token type
+ * @param expiresIn token expiration
+ * @param scope the scope granted for the access token, or null if not provided
+ * @param domain the Auth0 domain that issued these tokens
+ * @param issuer the issuer URL from the ID token
+ */
+ public Tokens(String accessToken, String idToken, String refreshToken, String type, Long expiresIn, String scope, String domain, String issuer) {
this.accessToken = accessToken;
this.idToken = idToken;
this.refreshToken = refreshToken;
this.type = type;
this.expiresIn = expiresIn;
+ this.scope = scope;
this.domain = domain;
this.issuer = issuer;
}
@@ -103,6 +122,15 @@ public Long getExpiresIn() {
return expiresIn;
}
+ /**
+ * Getter for the scope granted for the Access Token.
+ *
+ * @return the granted scope, or null if not provided.
+ */
+ public String getScope() {
+ return scope;
+ }
+
/**
* Getter for the Auth0 domain that issued these tokens.
diff --git a/src/test/java/com/auth0/AuthenticationControllerTest.java b/src/test/java/com/auth0/AuthenticationControllerTest.java
index 6af69ac..b123824 100644
--- a/src/test/java/com/auth0/AuthenticationControllerTest.java
+++ b/src/test/java/com/auth0/AuthenticationControllerTest.java
@@ -250,6 +250,94 @@ public void shouldThrowExceptionWhenBuildAuthorizeUrlRedirectUriIsNull() {
assertThat(exception.getMessage(), is("redirectUri must not be null"));
}
+ // --- renewAuth Tests ---
+
+ @Test
+ public void shouldRenewAuthWithDomain() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ RenewAuthRequest mockRenewAuthRequest = mock(RenewAuthRequest.class);
+ when(mockRequestProcessor.buildRenewAuthRequest("refreshToken", DOMAIN)).thenReturn(mockRenewAuthRequest);
+
+ RenewAuthRequest result = controller.renewAuth("refreshToken", DOMAIN);
+
+ assertThat(result, is(mockRenewAuthRequest));
+ verify(mockRequestProcessor).buildRenewAuthRequest("refreshToken", DOMAIN);
+ }
+
+ @Test
+ public void shouldRenewAuthWithoutDomain() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ RenewAuthRequest mockRenewAuthRequest = mock(RenewAuthRequest.class);
+ when(mockRequestProcessor.buildRenewAuthRequest("refreshToken")).thenReturn(mockRenewAuthRequest);
+
+ RenewAuthRequest result = controller.renewAuth("refreshToken");
+
+ assertThat(result, is(mockRenewAuthRequest));
+ verify(mockRequestProcessor).buildRenewAuthRequest("refreshToken");
+ }
+
+ @Test
+ public void shouldThrowExceptionWhenRenewAuthRefreshTokenIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.renewAuth(null, DOMAIN));
+ assertThat(exception.getMessage(), is("refreshToken must not be null"));
+ }
+
+ @Test
+ public void shouldThrowExceptionWhenRenewAuthDomainIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.renewAuth("refreshToken", (String) null));
+ assertThat(exception.getMessage(), is("domain must not be null"));
+ }
+
+ @Test
+ public void shouldThrowExceptionWhenNoArgRenewAuthRefreshTokenIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.renewAuth(null));
+ assertThat(exception.getMessage(), is("refreshToken must not be null"));
+ }
+
+ @Test
+ public void shouldRenewAuthWithRequest() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+ RenewAuthRequest mockRenewAuthRequest = mock(RenewAuthRequest.class);
+ when(mockRequestProcessor.buildRenewAuthRequest("refreshToken", request)).thenReturn(mockRenewAuthRequest);
+
+ RenewAuthRequest result = controller.renewAuth("refreshToken", request);
+
+ assertThat(result, is(mockRenewAuthRequest));
+ verify(mockRequestProcessor).buildRenewAuthRequest("refreshToken", request);
+ }
+
+ @Test
+ public void shouldThrowExceptionWhenRenewAuthWithRequestRefreshTokenIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.renewAuth((String) null, request));
+ assertThat(exception.getMessage(), is("refreshToken must not be null"));
+ }
+
+ @Test
+ public void shouldThrowExceptionWhenRenewAuthRequestIsNull() {
+ AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
+
+ NullPointerException exception = assertThrows(
+ NullPointerException.class,
+ () -> controller.renewAuth("refreshToken", (HttpServletRequest) null));
+ assertThat(exception.getMessage(), is("request must not be null"));
+ }
+
// --- Logging and Telemetry Tests ---
@Test
@@ -270,8 +358,6 @@ public void shouldDisableTelemetry() {
verify(mockRequestProcessor).doNotSendTelemetry();
}
- // --- Exception Propagation ---
-
@Test
public void shouldPropagateIdentityVerificationException() throws IdentityVerificationException {
AuthenticationController controller = new AuthenticationController(mockRequestProcessor);
diff --git a/src/test/java/com/auth0/RenewAuthRequestTest.java b/src/test/java/com/auth0/RenewAuthRequestTest.java
new file mode 100644
index 0000000..5f6dbf5
--- /dev/null
+++ b/src/test/java/com/auth0/RenewAuthRequestTest.java
@@ -0,0 +1,103 @@
+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.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class RenewAuthRequestTest {
+
+ private static final String REFRESH_TOKEN = "refreshToken";
+ 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