diff --git a/auth0/src/main/java/com/auth0/android/provider/LogoutManager.kt b/auth0/src/main/java/com/auth0/android/provider/LogoutManager.kt index 327f2b7d0..824c73713 100644 --- a/auth0/src/main/java/com/auth0/android/provider/LogoutManager.kt +++ b/auth0/src/main/java/com/auth0/android/provider/LogoutManager.kt @@ -15,6 +15,7 @@ internal class LogoutManager( ctOptions: CustomTabsOptions, federated: Boolean = false, private val launchAsTwa: Boolean = false, + private val customLogoutUrl: String? = null ) : ResumableManager() { private val parameters: MutableMap private val ctOptions: CustomTabsOptions @@ -42,7 +43,8 @@ internal class LogoutManager( } private fun buildLogoutUri(): Uri { - val logoutUri = Uri.parse(account.logoutUrl) + val urlToUse = customLogoutUrl ?: account.logoutUrl + val logoutUri = Uri.parse(urlToUse) val builder = logoutUri.buildUpon() for ((key, value) in parameters) { builder.appendQueryParameter(key, value) diff --git a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt index 6fba5152d..3413c99aa 100644 --- a/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt +++ b/auth0/src/main/java/com/auth0/android/provider/OAuthManager.kt @@ -25,6 +25,7 @@ internal class OAuthManager( parameters: Map, ctOptions: CustomTabsOptions, private val launchAsTwa: Boolean = false, + private val customAuthorizeUrl: String? = null ) : ResumableManager() { private val parameters: MutableMap private val headers: MutableMap @@ -197,6 +198,7 @@ internal class OAuthManager( auth0 = account, idTokenVerificationIssuer = idTokenVerificationIssuer, idTokenVerificationLeeway = idTokenVerificationLeeway, + customAuthorizeUrl = this.customAuthorizeUrl ) } @@ -235,7 +237,8 @@ internal class OAuthManager( } private fun buildAuthorizeUri(): Uri { - val authorizeUri = Uri.parse(account.authorizeUrl) + val urlToUse = customAuthorizeUrl ?: account.authorizeUrl + val authorizeUri = Uri.parse(urlToUse) val builder = authorizeUri.buildUpon() for ((key, value) in parameters) { builder.appendQueryParameter(key, value) @@ -357,7 +360,8 @@ internal fun OAuthManager.Companion.fromState( account = state.auth0, ctOptions = state.ctOptions, parameters = state.parameters, - callback = callback + callback = callback, + customAuthorizeUrl = state.customAuthorizeUrl ).apply { setHeaders( state.headers diff --git a/auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt b/auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt index 2b902bd04..2a2c71767 100644 --- a/auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt +++ b/auth0/src/main/java/com/auth0/android/provider/OAuthManagerState.kt @@ -17,7 +17,8 @@ internal data class OAuthManagerState( val ctOptions: CustomTabsOptions, val pkce: PKCE?, val idTokenVerificationLeeway: Int?, - val idTokenVerificationIssuer: String? + val idTokenVerificationIssuer: String?, + val customAuthorizeUrl: String? = null ) { private class OAuthManagerJson( @@ -32,7 +33,8 @@ internal data class OAuthManagerState( val codeChallenge: String, val codeVerifier: String, val idTokenVerificationLeeway: Int?, - val idTokenVerificationIssuer: String? + val idTokenVerificationIssuer: String?, + val customAuthorizeUrl: String? = null ) fun serializeToJson( @@ -56,6 +58,7 @@ internal data class OAuthManagerState( codeChallenge = pkce?.codeChallenge.orEmpty(), idTokenVerificationIssuer = idTokenVerificationIssuer, idTokenVerificationLeeway = idTokenVerificationLeeway, + customAuthorizeUrl = this.customAuthorizeUrl ) return gson.toJson(json) } finally { @@ -103,6 +106,7 @@ internal data class OAuthManagerState( ), idTokenVerificationIssuer = oauthManagerJson.idTokenVerificationIssuer, idTokenVerificationLeeway = oauthManagerJson.idTokenVerificationLeeway, + customAuthorizeUrl = oauthManagerJson.customAuthorizeUrl ) } finally { parcel.recycle() diff --git a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt index a46f0df6b..dff8a48a9 100644 --- a/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt +++ b/auth0/src/main/java/com/auth0/android/provider/WebAuthProvider.kt @@ -148,6 +148,7 @@ public object WebAuthProvider { private var ctOptions: CustomTabsOptions = CustomTabsOptions.newBuilder().build() private var federated: Boolean = false private var launchAsTwa: Boolean = false + private var customLogoutUrl: String? = null /** * When using a Custom Tabs compatible Browser, apply these customization options. @@ -215,6 +216,18 @@ public object WebAuthProvider { return this } + /** + * Specifies a custom Logout URL to use for this logout request, overriding the default + * generated from the Auth0 domain (account.logoutUrl). + * + * @param logoutUrl the custom logout URL. + * @return the current builder instance + */ + public fun withLogoutUrl(logoutUrl: String): LogoutBuilder { + this.customLogoutUrl = logoutUrl + return this + } + /** * Request the user session to be cleared. When successful, the callback will get invoked. * An error is raised if there are no browser applications installed in the device or if @@ -248,7 +261,8 @@ public object WebAuthProvider { returnToUrl!!, ctOptions, federated, - launchAsTwa + launchAsTwa, + customLogoutUrl ) managerInstance = logoutManager logoutManager.startLogout(context) @@ -294,6 +308,7 @@ public object WebAuthProvider { private var ctOptions: CustomTabsOptions = CustomTabsOptions.newBuilder().build() private var leeway: Int? = null private var launchAsTwa: Boolean = false + private var customAuthorizeUrl: String? = null /** * Use a custom state in the requests @@ -507,6 +522,18 @@ public object WebAuthProvider { return this } + /** + * Specifies a custom Authorize URL to use for this login request, overriding the default + * generated from the Auth0 domain (account.authorizeUrl). + * + * @param authorizeUrl the custom authorize URL. + * @return the current builder instance + */ + public fun withAuthorizeUrl(authorizeUrl: String): Builder { + this.customAuthorizeUrl = authorizeUrl + return this + } + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun withPKCE(pkce: PKCE): Builder { this.pkce = pkce @@ -553,7 +580,8 @@ public object WebAuthProvider { values[OAuthManager.KEY_ORGANIZATION] = organizationId values[OAuthManager.KEY_INVITATION] = invitationId } - val manager = OAuthManager(account, callback, values, ctOptions, launchAsTwa) + val manager = OAuthManager(account, callback, values, ctOptions, launchAsTwa, + customAuthorizeUrl) manager.setHeaders(headers) manager.setPKCE(pkce) manager.setIdTokenVerificationLeeway(leeway) diff --git a/auth0/src/test/java/com/auth0/android/provider/LogoutManagerTest.java b/auth0/src/test/java/com/auth0/android/provider/LogoutManagerTest.java index 25faa8926..d5996e329 100644 --- a/auth0/src/test/java/com/auth0/android/provider/LogoutManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/LogoutManagerTest.java @@ -38,7 +38,7 @@ public void setUp() { @Test public void shouldCallOnFailureWhenResumedWithCanceledResult() { - LogoutManager manager = new LogoutManager(account, callback, "https://auth0.com/android/my.app.name/callback", customTabsOptions, false, false); + LogoutManager manager = new LogoutManager(account, callback, "https://auth0.com/android/my.app.name/callback", customTabsOptions, false, false,null); AuthorizeResult result = mock(AuthorizeResult.class); when(result.isCanceled()).thenReturn(true); manager.resume(result); @@ -51,7 +51,7 @@ public void shouldCallOnFailureWhenResumedWithCanceledResult() { @Test public void shouldCallOnSuccessWhenResumedWithValidResult() { - LogoutManager manager = new LogoutManager(account, callback, "https://auth0.com/android/my.app.name/callback", customTabsOptions, false, false); + LogoutManager manager = new LogoutManager(account, callback, "https://auth0.com/android/my.app.name/callback", customTabsOptions, false, false,null); AuthorizeResult result = mock(AuthorizeResult.class); when(result.isCanceled()).thenReturn(false); manager.resume(result); diff --git a/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java b/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java index fc95efa0f..e7a21947a 100644 --- a/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/OAuthManagerTest.java @@ -1,16 +1,67 @@ package com.auth0.android.provider; +import android.net.Uri; +import com.auth0.android.Auth0; import com.auth0.android.authentication.AuthenticationException; +import com.auth0.android.callback.Callback; +import com.auth0.android.request.NetworkingClient; +import com.auth0.android.util.Auth0UserAgent; +import com.auth0.android.result.Credentials; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; + @RunWith(RobolectricTestRunner.class) public class OAuthManagerTest { + @Mock + private Auth0 mockAccount; + @Mock + private Callback mockCallback; + @Mock + private CustomTabsOptions mockCtOptions; + @Mock + private OAuthManagerState mockState; + @Mock + private PKCE mockPkce; + @Mock + private NetworkingClient mockNetworkingClient; + @Mock + private Auth0UserAgent mockUserAgent; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + Mockito.when(mockAccount.getNetworkingClient()).thenReturn(mockNetworkingClient); + Mockito.when(mockAccount.getClientId()).thenReturn("test-client-id"); + Mockito.when(mockAccount.getDomainUrl()).thenReturn("https://test.domain.com/"); + Mockito.when(mockAccount.getAuth0UserAgent()).thenReturn(mockUserAgent); + Mockito.when(mockUserAgent.getValue()).thenReturn("test-user-agent/1.0"); + Mockito.when(mockState.getAuth0()).thenReturn(mockAccount); + Mockito.when(mockState.getCtOptions()).thenReturn(mockCtOptions); + Mockito.when(mockState.getParameters()).thenReturn(Collections.emptyMap()); + Mockito.when(mockState.getHeaders()).thenReturn(Collections.emptyMap()); + Mockito.when(mockState.getPkce()).thenReturn(mockPkce); + Mockito.when(mockState.getIdTokenVerificationIssuer()).thenReturn("default-issuer"); + Mockito.when(mockState.getIdTokenVerificationLeeway()).thenReturn(60); + } + + @After + public void tearDown() { + } + @Test public void shouldHaveValidState() { OAuthManager.assertValidState("1234567890", "1234567890"); @@ -25,4 +76,80 @@ public void shouldHaveInvalidState() { public void shouldHaveInvalidStateWhenOneIsNull() { Assert.assertThrows(AuthenticationException.class, () -> OAuthManager.assertValidState("0987654321", null)); } + + @Test + public void buildAuthorizeUriShouldUseDefaultUrlWhenCustomUrlIsNull() throws Exception { + final String defaultUrl = "https://default.auth0.com/authorize"; + final Map parameters = Collections.singletonMap("param1", "value1"); + Mockito.when(mockAccount.getAuthorizeUrl()).thenReturn(defaultUrl); + OAuthManager manager = new OAuthManager(mockAccount, mockCallback, parameters, mockCtOptions, false, null); + Uri resultUri = callBuildAuthorizeUri(manager); + Assert.assertNotNull(resultUri); + Assert.assertEquals("https", resultUri.getScheme()); + Assert.assertEquals("default.auth0.com", resultUri.getHost()); + Assert.assertEquals("/authorize", resultUri.getPath()); + Assert.assertEquals("value1", resultUri.getQueryParameter("param1")); + Mockito.verify(mockAccount).getAuthorizeUrl(); + } + + @Test + public void buildAuthorizeUriShouldUseCustomUrlWhenProvided() throws Exception { + final String defaultUrl = "https://default.auth0.com/authorize"; + final String customUrl = "https://custom.example.com/custom_auth"; + final Map parameters = Collections.singletonMap("param1", "value1"); + Mockito.when(mockAccount.getAuthorizeUrl()).thenReturn(defaultUrl); + OAuthManager manager = new OAuthManager(mockAccount, mockCallback, parameters, mockCtOptions, false, customUrl); + Uri resultUri = callBuildAuthorizeUri(manager); + Assert.assertNotNull(resultUri); + Assert.assertEquals("https", resultUri.getScheme()); + Assert.assertEquals("custom.example.com", resultUri.getHost()); + Assert.assertEquals("/custom_auth", resultUri.getPath()); + Assert.assertEquals("value1", resultUri.getQueryParameter("param1")); + Mockito.verify(mockAccount, Mockito.never()).getAuthorizeUrl(); + } + + @Test + public void managerRestoredFromStateShouldUseRestoredCustomAuthorizeUrl() throws Exception { + final String restoredCustomUrl = "https://restored.com/custom_auth"; + final String defaultUrl = "https://should-not-be-used.com/authorize"; + + Mockito.when(mockState.getCustomAuthorizeUrl()).thenReturn(restoredCustomUrl); + Mockito.when(mockAccount.getAuthorizeUrl()).thenReturn(defaultUrl); + + OAuthManager restoredManager = new OAuthManager( + mockState.getAuth0(), mockCallback, mockState.getParameters(), + mockState.getCtOptions(), false, mockState.getCustomAuthorizeUrl() + ); + Uri resultUri = callBuildAuthorizeUri(restoredManager); + Assert.assertNotNull(resultUri); + Assert.assertEquals("https", resultUri.getScheme()); + Assert.assertEquals("restored.com", resultUri.getHost()); + Assert.assertEquals("/custom_auth", resultUri.getPath()); + Mockito.verify(mockAccount, Mockito.never()).getAuthorizeUrl(); + } + + @Test + public void managerRestoredFromStateShouldHandleNullCustomAuthorizeUrl() throws Exception { + final String defaultUrl = "https://default.auth0.com/authorize"; + + Mockito.when(mockState.getCustomAuthorizeUrl()).thenReturn(null); + Mockito.when(mockAccount.getAuthorizeUrl()).thenReturn(defaultUrl); + OAuthManager restoredManager = new OAuthManager( + mockState.getAuth0(), mockCallback, mockState.getParameters(), + mockState.getCtOptions(), false, mockState.getCustomAuthorizeUrl() + ); + Uri resultUri = callBuildAuthorizeUri(restoredManager); + Assert.assertNotNull(resultUri); + Assert.assertEquals("https", resultUri.getScheme()); + Assert.assertEquals("default.auth0.com", resultUri.getHost()); + Assert.assertEquals("/authorize", resultUri.getPath()); + Mockito.verify(mockAccount).getAuthorizeUrl(); + } + + private Uri callBuildAuthorizeUri(OAuthManager instance) throws Exception { + Method method = OAuthManager.class.getDeclaredMethod("buildAuthorizeUri"); + method.setAccessible(true); + return (Uri) method.invoke(instance); + } + } \ No newline at end of file diff --git a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt index dbda94350..4b9fc7e6e 100644 --- a/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt +++ b/auth0/src/test/java/com/auth0/android/provider/WebAuthProviderTest.kt @@ -72,6 +72,9 @@ public class WebAuthProviderTest { private val callbackCaptor: KArgumentCaptor> = argumentCaptor() + private val customAuthorizeUrl = "https://custom.domain.com/custom_auth" + private val customLogoutUrl = "https://custom.domain.com/custom_logout" + @Before public fun setUp() { MockitoAnnotations.openMocks(this) @@ -106,6 +109,24 @@ public class WebAuthProviderTest { Assert.assertNotNull(WebAuthProvider.managerInstance) } + @Test + public fun shouldSetCustomAuthorizeUrlOnLogin() { + login(account) + .withAuthorizeUrl(customAuthorizeUrl) + .start(activity, callback) + verify(activity).startActivity(intentCaptor.capture()) + val uri = + intentCaptor.firstValue.getParcelableExtra(AuthenticationActivity.EXTRA_AUTHORIZE_URI) + assertThat(uri, `is`(notNullValue())) + assertThat(uri?.scheme, `is`("https")) + assertThat(uri?.host, `is`("custom.domain.com")) + assertThat(uri?.path, `is`("/custom_auth")) + assertThat(uri, UriMatchers.hasParamWithName("client_id")) + assertThat(uri, UriMatchers.hasParamWithName("redirect_uri")) + assertThat(uri, UriMatchers.hasParamWithName("response_type")) + assertThat(uri, UriMatchers.hasParamWithName("state")) + } + //scheme @Test public fun shouldHaveDefaultSchemeOnLogin() { @@ -2326,6 +2347,22 @@ public class WebAuthProviderTest { Assert.assertNotNull(WebAuthProvider.managerInstance) } + @Test + public fun shouldSetCustomLogoutUrlOnLogout() { + logout(account) + .withLogoutUrl(customLogoutUrl) + .start(activity, voidCallback) + verify(activity).startActivity(intentCaptor.capture()) + val uri = + intentCaptor.firstValue.getParcelableExtra(AuthenticationActivity.EXTRA_AUTHORIZE_URI) + assertThat(uri, `is`(notNullValue())) + assertThat(uri?.scheme, `is`("https")) + assertThat(uri?.host, `is`("custom.domain.com")) + assertThat(uri?.path, `is`("/custom_logout")) + assertThat(uri, UriMatchers.hasParamWithName("client_id")) + assertThat(uri, UriMatchers.hasParamWithName("returnTo")) + } + //scheme @Test public fun shouldHaveDefaultSchemeOnLogout() { @@ -2612,6 +2649,22 @@ public class WebAuthProviderTest { job.join() } + @Test + public fun shouldBuildDefaultLogoutURIWithCorrectSchemeHostAndPathOnLogout() { + logout(account) + .start(activity, voidCallback) + val baseUriString = Uri.parse(account.logoutUrl) + verify(activity).startActivity(intentCaptor.capture()) + val uri = + intentCaptor.firstValue.getParcelableExtra(AuthenticationActivity.EXTRA_AUTHORIZE_URI) + assertThat(uri, `is`(notNullValue())) + assertThat(uri, UriMatchers.hasScheme(baseUriString.scheme)) + assertThat(uri, UriMatchers.hasHost(baseUriString.host)) + assertThat(uri, UriMatchers.hasPath(baseUriString.path)) + assertThat(uri, UriMatchers.hasParamWithName("client_id")) + assertThat(uri, UriMatchers.hasParamWithName("returnTo")) + } + //** ** ** ** ** ** **// //** ** ** ** ** ** **// //** Helpers Functions**//