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 mockTokenResponse; + @Mock + private TokenHolder mockTokenHolder; + + @BeforeEach + public void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + when(mockClient.renewAuth(REFRESH_TOKEN)).thenReturn(mockTokenRequest); + when(mockTokenRequest.execute()).thenReturn(mockTokenResponse); + when(mockTokenResponse.getBody()).thenReturn(mockTokenHolder); + } + + @Test + public void shouldNotSetAudienceOrScopeWhenNotProvided() throws Exception { + RenewAuthRequest request = new RenewAuthRequest(mockClient, REFRESH_TOKEN, DOMAIN, ISSUER); + + request.execute(); + + verify(mockTokenRequest, never()).setAudience(org.mockito.ArgumentMatchers.anyString()); + verify(mockTokenRequest, never()).setScope(org.mockito.ArgumentMatchers.anyString()); + } + + @Test + public void shouldSetAudienceWhenProvided() throws Exception { + RenewAuthRequest request = new RenewAuthRequest(mockClient, REFRESH_TOKEN, DOMAIN, ISSUER); + + request.withAudience("https://api.example.com").execute(); + + verify(mockTokenRequest).setAudience("https://api.example.com"); + verify(mockTokenRequest, never()).setScope(org.mockito.ArgumentMatchers.anyString()); + } + + @Test + public void shouldSetScopeWhenProvided() throws Exception { + RenewAuthRequest request = new RenewAuthRequest(mockClient, REFRESH_TOKEN, DOMAIN, ISSUER); + + request.withScope("openid profile").execute(); + + verify(mockTokenRequest).setScope("openid profile"); + verify(mockTokenRequest, never()).setAudience(org.mockito.ArgumentMatchers.anyString()); + } + + @Test + public void shouldMapTokenHolderToTokensIncludingScope() throws Exception { + when(mockTokenHolder.getAccessToken()).thenReturn("newAccessToken"); + when(mockTokenHolder.getIdToken()).thenReturn(null); + when(mockTokenHolder.getRefreshToken()).thenReturn("rotatedRefreshToken"); + when(mockTokenHolder.getTokenType()).thenReturn("Bearer"); + when(mockTokenHolder.getExpiresIn()).thenReturn(86400L); + when(mockTokenHolder.getScope()).thenReturn("openid profile"); + + RenewAuthRequest request = new RenewAuthRequest(mockClient, REFRESH_TOKEN, DOMAIN, ISSUER); + + Tokens tokens = request.withAudience("https://api.example.com").withScope("openid profile").execute(); + + assertThat(tokens.getAccessToken(), is("newAccessToken")); + assertThat(tokens.getRefreshToken(), is("rotatedRefreshToken")); + assertThat(tokens.getType(), is("Bearer")); + assertThat(tokens.getExpiresIn(), is(86400L)); + assertThat(tokens.getScope(), is("openid profile")); + assertThat(tokens.getDomain(), is(DOMAIN)); + assertThat(tokens.getIssuer(), is(ISSUER)); + } + + @Test + public void shouldPropagateAuth0Exception() throws Exception { + when(mockTokenRequest.execute()).thenThrow(Auth0Exception.class); + + RenewAuthRequest request = new RenewAuthRequest(mockClient, REFRESH_TOKEN, DOMAIN, ISSUER); + + assertThrows(Auth0Exception.class, request::execute); + } +} diff --git a/src/test/java/com/auth0/RequestProcessorTest.java b/src/test/java/com/auth0/RequestProcessorTest.java index 0851418..a414307 100644 --- a/src/test/java/com/auth0/RequestProcessorTest.java +++ b/src/test/java/com/auth0/RequestProcessorTest.java @@ -147,6 +147,64 @@ public void shouldCreateClientForDomain() { assertThat(result, is(notNullValue())); } + // --- RenewAuth Tests --- + + @Test + public void shouldBuildRenewAuthRequestForExplicitDomain() { + RequestProcessor processor = createDefaultRequestProcessor(); + RequestProcessor spy = spy(processor); + doReturn(mockAuthAPI).when(spy).createClientForDomain(anyString()); + + RenewAuthRequest result = spy.buildRenewAuthRequest("refreshToken", DOMAIN); + + assertThat(result, is(notNullValue())); + verify(spy).createClientForDomain(DOMAIN); + } + + @Test + public void shouldBuildRenewAuthRequestFromStaticDomain() { + RequestProcessor processor = new RequestProcessor.Builder( + new StaticDomainProvider(DOMAIN), + RESPONSE_TYPE_CODE, + CLIENT_ID, + CLIENT_SECRET) + .withJwkProvider(mockJwkProvider) + .build(); + RequestProcessor spy = spy(processor); + doReturn(mockAuthAPI).when(spy).createClientForDomain(anyString()); + + RenewAuthRequest result = spy.buildRenewAuthRequest("refreshToken"); + + assertThat(result, is(notNullValue())); + verify(spy).createClientForDomain(DOMAIN); + } + + @Test + public void shouldThrowOnNoArgRenewAuthWhenUsingResolver() { + RequestProcessor processor = createDefaultRequestProcessor(); + + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> processor.buildRenewAuthRequest("refreshToken")); + assertThat(exception.getMessage(), containsString("A domain is required when using a DomainResolver")); + } + + @Test + public void shouldBuildRenewAuthRequestResolvingDomainFromRequest() { + String resolvedDomain = "resolved-domain.auth0.com"; + when(mockDomainProvider.getDomain(request)).thenReturn(resolvedDomain); + + RequestProcessor processor = createDefaultRequestProcessor(); + RequestProcessor spy = spy(processor); + doReturn(mockAuthAPI).when(spy).createClientForDomain(anyString()); + + RenewAuthRequest result = spy.buildRenewAuthRequest("refreshToken", request); + + assertThat(result, is(notNullValue())); + verify(mockDomainProvider).getDomain(request); + verify(spy).createClientForDomain(resolvedDomain); + } + // --- Logging and Telemetry Tests --- @Test diff --git a/src/test/java/com/auth0/TokensTest.java b/src/test/java/com/auth0/TokensTest.java index c82a0c1..6bcc3d2 100644 --- a/src/test/java/com/auth0/TokensTest.java +++ b/src/test/java/com/auth0/TokensTest.java @@ -31,4 +31,31 @@ public void shouldReturnMissingTokens() { assertThat(tokens.getDomain(), is(nullValue())); assertThat(tokens.getIssuer(), is(nullValue())); } + + @Test + public void shouldHaveNullScopeFromFiveArgConstructor() { + Tokens tokens = new Tokens("accessToken", "idToken", "refreshToken", "bearer", 360000L); + assertThat(tokens.getScope(), is(nullValue())); + } + + @Test + public void shouldHaveNullScopeFromSevenArgConstructor() { + Tokens tokens = new Tokens("accessToken", "idToken", "refreshToken", "bearer", 360000L, "domain.auth0.com", "https://domain.auth0.com/"); + assertThat(tokens.getScope(), is(nullValue())); + assertThat(tokens.getDomain(), is("domain.auth0.com")); + assertThat(tokens.getIssuer(), is("https://domain.auth0.com/")); + } + + @Test + public void shouldReturnScopeFromEightArgConstructor() { + Tokens tokens = new Tokens("accessToken", "idToken", "refreshToken", "bearer", 360000L, "openid profile", "domain.auth0.com", "https://domain.auth0.com/"); + assertThat(tokens.getAccessToken(), is("accessToken")); + assertThat(tokens.getIdToken(), is("idToken")); + assertThat(tokens.getRefreshToken(), is("refreshToken")); + assertThat(tokens.getType(), is("bearer")); + assertThat(tokens.getExpiresIn(), is(360000L)); + assertThat(tokens.getScope(), is("openid profile")); + assertThat(tokens.getDomain(), is("domain.auth0.com")); + assertThat(tokens.getIssuer(), is("https://domain.auth0.com/")); + } }