From f5bfafddb89acc12aa33b224d18a7e2da6c6a26f Mon Sep 17 00:00:00 2001 From: jey Date: Mon, 3 Jun 2024 18:30:31 -0500 Subject: [PATCH] OIDC ping discovery --- .../org/forgerock/android/auth/FROptions.kt | 78 +++++++++++- .../forgerock/android/auth/OAuth2Client.kt | 33 +++-- .../forgerock/android/auth/UserService.java | 15 ++- .../forgerock/android/auth/FROptionTest.kt | 119 +++++++++++++++++- .../src/test/resources/discovery.json | 29 +++++ .../android/auth/KotlinExtensions.kt | 22 +++- .../forgerock/android/auth/ExtensionTest.kt | 70 +++++++++++ .../main/java/com/example/app/AppNavHost.kt | 5 +- .../java/com/example/app/env/EnvViewModel.kt | 27 +++- 9 files changed, 377 insertions(+), 21 deletions(-) create mode 100644 forgerock-auth/src/test/resources/discovery.json create mode 100644 forgerock-core/src/test/java/org/forgerock/android/auth/ExtensionTest.kt diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/FROptions.kt b/forgerock-auth/src/main/java/org/forgerock/android/auth/FROptions.kt index 39cc2abf..86fe7c0a 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/FROptions.kt +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/FROptions.kt @@ -6,7 +6,15 @@ */ package org.forgerock.android.auth +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Interceptor import okhttp3.OkHttpClient +import okhttp3.Request +import org.forgerock.android.auth.OkHttpClientProvider.getInstance +import org.forgerock.android.auth.exception.ApiException +import org.json.JSONObject +import java.net.URL /** * Manages SDK configuration information @@ -22,7 +30,7 @@ data class FROptions(val server: Server, @JvmStatic fun equals(old: FROptions?, new: FROptions?): Boolean { // do the referential check first - if(old === new) { + if (old === new) { return true } // if there is a change in reference then check the value @@ -34,6 +42,7 @@ data class FROptions(val server: Server, && old?.logger == new?.logger } } + @Throws(IllegalArgumentException::class) @JvmName("validateConfig") internal fun validateConfig() { @@ -41,6 +50,67 @@ data class FROptions(val server: Server, require(server.realm.isNotBlank()) { "Realm cannot be blank" } require(server.cookieName.isNotBlank()) { "cookieName cannot be blank" } } + private fun getOkHttpClient(url: URL): OkHttpClient { + val networkConfig = NetworkConfig.networkBuilder() + .host(url.authority) + .interceptorSupplier { + listOf( + OkHttpRequestInterceptor() + ) + } + .build() + // Obtain instance of OkHttp client + val httpClient = getInstance().lookup(networkConfig) + return httpClient + } + + /** + * Discover the OpenID Configuration + * + * @param discoverUrl The OpenID Configuration URL + * @return The SDK configuration information + */ + suspend fun discover(discoverUrl: String): FROptions { + return withContext(Dispatchers.IO) { + try { + val url = URL(discoverUrl) + val httpClient = getOkHttpClient(url) + val request = Request.Builder() + .url(discoverUrl) + .get().build() + + httpClient.newCall(request).execute().use { response -> + if (response.isSuccessful.not()) { + throw ApiException( + response.code, + response.message, + response.body?.string() + ) + } + + val openIdConfiguration = response.body?.string()?.let { + JSONObject(it) + } + + val issuer = openIdConfiguration?.getString("issuer") ?: "" + val server = this@FROptions.server.copy(url = (server.url.isNotEmpty() then server.url) + ?: issuer) + val urlPath = this@FROptions.urlPath.copy( + authorizeEndpoint = openIdConfiguration?.getString("authorization_endpoint"), + tokenEndpoint = openIdConfiguration?.getString("token_endpoint"), + userinfoEndpoint = openIdConfiguration?.getString("userinfo_endpoint"), + revokeEndpoint = openIdConfiguration?.getString("revocation_endpoint"), + endSessionEndpoint = openIdConfiguration?.getString("end_session_endpoint")) + + this@FROptions.copy(urlPath = urlPath, server = server) + + } + } catch (e: Exception) { + throw e + } + } + } + } /** @@ -48,7 +118,7 @@ data class FROptions(val server: Server, */ class FROptionsBuilder { - private lateinit var server: Server + private var server: Server = Server("", "root", 30, "iPlanetDirectoryPro", 0) private var oauth: OAuth = OAuth() private var service: Service = Service() private var urlPath: UrlPath = UrlPath() @@ -123,7 +193,7 @@ class FROptionsBuilder { /** * Data class for the server configurations */ -data class Server(val url: String, +data class Server(val url: String = "", val realm: String = "root", val timeout: Int = 30, val cookieName: String = "iPlanetDirectoryPro", @@ -133,7 +203,7 @@ data class Server(val url: String, * Server builder to build the SDK configuration information specific to server */ class ServerBuilder { - lateinit var url: String + var url: String = "" var realm: String = "root" var timeout: Int = 30 var cookieName: String = "iPlanetDirectoryPro" diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.kt b/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.kt index 626c5be5..dad06b92 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.kt +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.kt @@ -316,9 +316,13 @@ class OAuth2Client( @get:Throws(MalformedURLException::class) val authorizeUrl: URL get() { + val authorizeEndpoint = serverConfig.authorizeEndpoint + if (authorizeEndpoint.isAbsoluteUrl()) { + return URL(authorizeEndpoint) + } val builder = Uri.parse(serverConfig.url).buildUpon() - if (StringUtils.isNotEmpty(serverConfig.authorizeEndpoint)) { - builder.appendEncodedPath(serverConfig.authorizeEndpoint) + if (StringUtils.isNotEmpty(authorizeEndpoint)) { + builder.appendEncodedPath(authorizeEndpoint) } else { builder.appendPath("oauth2") .appendPath("realms") @@ -331,9 +335,13 @@ class OAuth2Client( @get:Throws(MalformedURLException::class) val tokenUrl: URL get() { + val tokenEndpoint = serverConfig.tokenEndpoint + if (tokenEndpoint.isAbsoluteUrl()) { + return URL(tokenEndpoint) + } val builder = Uri.parse(serverConfig.url).buildUpon() - if (StringUtils.isNotEmpty(serverConfig.tokenEndpoint)) { - builder.appendEncodedPath(serverConfig.tokenEndpoint) + if (StringUtils.isNotEmpty(tokenEndpoint)) { + builder.appendEncodedPath(tokenEndpoint) } else { builder.appendPath("oauth2") .appendPath("realms") @@ -346,9 +354,14 @@ class OAuth2Client( @get:Throws(MalformedURLException::class) val revokeUrl: URL get() { + val revokeEndpoint = serverConfig.revokeEndpoint + + if (revokeEndpoint.isAbsoluteUrl()) { + return URL(revokeEndpoint) + } val builder = Uri.parse(serverConfig.url).buildUpon() - if (StringUtils.isNotEmpty(serverConfig.revokeEndpoint)) { - builder.appendEncodedPath(serverConfig.revokeEndpoint) + if (StringUtils.isNotEmpty(revokeEndpoint)) { + builder.appendEncodedPath(revokeEndpoint) } else { builder.appendPath("oauth2") .appendPath("realms") @@ -362,9 +375,13 @@ class OAuth2Client( @get:Throws(MalformedURLException::class) val endSessionUrl: URL get() { + val sessionEndpoint = serverConfig.endSessionEndpoint + if (sessionEndpoint.isAbsoluteUrl()) { + return URL(sessionEndpoint) + } val builder = Uri.parse(serverConfig.url).buildUpon() - if (StringUtils.isNotEmpty(serverConfig.endSessionEndpoint)) { - builder.appendEncodedPath(serverConfig.endSessionEndpoint) + if (StringUtils.isNotEmpty(sessionEndpoint)) { + builder.appendEncodedPath(sessionEndpoint) } else { builder.appendPath("oauth2") .appendPath("realms") diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/UserService.java b/forgerock-auth/src/main/java/org/forgerock/android/auth/UserService.java index 9842ec3b..8ab09154 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/UserService.java +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/UserService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 - 2022 ForgeRock. All rights reserved. + * Copyright (c) 2019 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -7,7 +7,9 @@ package org.forgerock.android.auth; + import android.net.Uri; + import lombok.Builder; import okhttp3.Call; import okhttp3.OkHttpClient; @@ -21,6 +23,7 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import static org.forgerock.android.auth.KotlinExtensionsKt.isAbsoluteUrl; /** * Service Client @@ -84,9 +87,15 @@ public void onResponse(@NotNull Call call, @NotNull Response response) { private URL getUserInfoUrl() throws MalformedURLException { + String userInfoEndpoint = serverConfig.getUserInfoEndpoint(); + + if(isAbsoluteUrl(userInfoEndpoint)) { + return new URL(userInfoEndpoint); + } + Uri.Builder builder = Uri.parse(serverConfig.getUrl()).buildUpon(); - if (StringUtils.isNotEmpty(serverConfig.getUserInfoEndpoint())) { - builder.appendEncodedPath(serverConfig.getUserInfoEndpoint()); + if (StringUtils.isNotEmpty(userInfoEndpoint)) { + builder.appendEncodedPath(userInfoEndpoint); } else { builder.appendPath("oauth2") .appendPath("realms") diff --git a/forgerock-auth/src/test/java/org/forgerock/android/auth/FROptionTest.kt b/forgerock-auth/src/test/java/org/forgerock/android/auth/FROptionTest.kt index 84ddac78..08c112c1 100644 --- a/forgerock-auth/src/test/java/org/forgerock/android/auth/FROptionTest.kt +++ b/forgerock-auth/src/test/java/org/forgerock/android/auth/FROptionTest.kt @@ -1,17 +1,25 @@ /* - * Copyright (c) 2022 - 2023 ForgeRock. All rights reserved. + * Copyright (c) 2022 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ package org.forgerock.android.auth +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.runBlocking import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import org.forgerock.android.auth.exception.ApiException import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.Test +import org.junit.runner.RunWith +import java.net.HttpURLConnection -class FROptionTest { +@RunWith(AndroidJUnit4::class) +class FROptionTest: BaseTest() { @Test fun testDefaultBuilderOption() { class TestCustomLogger: FRLogger { @@ -84,6 +92,7 @@ class FROptionTest { }} option1.validateConfig() } + @Test fun testReferenceAndValue() { val option1 = FROptionsBuilder.build { server { @@ -100,4 +109,110 @@ class FROptionTest { assertFalse(FROptions.equals(option1, option2)) assertTrue(FROptions.equals(option2, option2)) } + + @Test + fun testDiscoverEndpointFailure() { + runBlocking { + val option1 = FROptionsBuilder.build { + server { + url = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as" + } + urlPath { + authenticateEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authenticate" + sessionEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/session" + } + oauth { + oauthClientId = "AndroidTest" + oauthRedirectUri = "org.forgerock.demo:/oauth2redirect" + oauthScope = "openid profile email address phone" + } + } + + server.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_FORBIDDEN)) + + try { + val copied = + option1.discover(url) + FRAuth.start(context, copied) + fail() + } + catch (e: ApiException) { + assertTrue(e.statusCode == HttpURLConnection.HTTP_FORBIDDEN) + } + } + } + @Test + fun testDiscoverEndpointWithURLProvided() { + runBlocking { + server.enqueue( + MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader("Content-Type", "application/json") + .setBody(getJson("/discovery.json"))) + + val option1 = FROptionsBuilder.build { + server { + url = "https://auth.pingone.us/02fb4743-189a-4bc7-9d6c-a919edfe6447/as" + } + urlPath { + authenticateEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authenticate" + sessionEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/session" + } + oauth { + oauthClientId = "AndroidTest" + oauthRedirectUri = "org.forgerock.demo:/oauth2redirect" + oauthScope = "openid profile email address phone" + } + } + val copied = option1.discover(url) + assertTrue(copied.urlPath.authorizeEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize") + assertTrue(copied.server.url == "https://auth.pingone.us/02fb4743-189a-4bc7-9d6c-a919edfe6447/as") + assertTrue(copied.urlPath.revokeEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/revoke") + assertTrue(copied.urlPath.tokenEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/token") + assertTrue(copied.urlPath.userinfoEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/userinfo") + assertTrue(copied.urlPath.endSessionEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/signoff") + assertTrue(copied.urlPath.sessionEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/session") + assertTrue(copied.urlPath.authenticateEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authenticate") + assertTrue(copied.oauth.oauthClientId == "AndroidTest") + assertTrue(copied.oauth.oauthRedirectUri == "org.forgerock.demo:/oauth2redirect") + assertTrue(copied.oauth.oauthScope == "openid profile email address phone") + } + } + + @Test + fun testDiscoverEndpointWithURLNotProvided() { + runBlocking { + server.enqueue( + MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .addHeader("Content-Type", "application/json") + .setBody(getJson("/discovery.json"))) + + val option1 = FROptionsBuilder.build { + urlPath { + authenticateEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authenticate" + sessionEndpoint = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/session" + } + oauth { + oauthClientId = "AndroidTest" + oauthRedirectUri = "org.forgerock.demo:/oauth2redirect" + oauthScope = "openid profile email address phone" + } + } + val copied = option1.discover(url) + assertTrue(copied.urlPath.authorizeEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize") + assertTrue(copied.server.url == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as") + assertTrue(copied.urlPath.revokeEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/revoke") + assertTrue(copied.urlPath.tokenEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/token") + assertTrue(copied.urlPath.userinfoEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/userinfo") + assertTrue(copied.urlPath.endSessionEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/signoff") + assertTrue(copied.urlPath.sessionEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/session") + assertTrue(copied.urlPath.authenticateEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authenticate") + assertTrue(copied.oauth.oauthClientId == "AndroidTest") + assertTrue(copied.oauth.oauthRedirectUri == "org.forgerock.demo:/oauth2redirect") + assertTrue(copied.oauth.oauthScope == "openid profile email address phone") + } + } + + } \ No newline at end of file diff --git a/forgerock-auth/src/test/resources/discovery.json b/forgerock-auth/src/test/resources/discovery.json new file mode 100644 index 00000000..cd94daae --- /dev/null +++ b/forgerock-auth/src/test/resources/discovery.json @@ -0,0 +1,29 @@ +{ + "issuer" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as", + "authorization_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize", + "pushed_authorization_request_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/par", + "token_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/token", + "userinfo_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/userinfo", + "jwks_uri" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/jwks", + "end_session_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/signoff", + "introspection_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/introspect", + "revocation_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/revoke", + "device_authorization_endpoint" : "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/device_authorization", + "claims_parameter_supported" : false, + "request_parameter_supported" : true, + "request_uri_parameter_supported" : false, + "require_pushed_authorization_requests" : false, + "scopes_supported" : [ "openid", "profile", "email", "address", "phone" ], + "response_types_supported" : [ "code", "id_token", "token id_token", "code id_token", "code token", "code token id_token" ], + "response_modes_supported" : [ "pi.flow", "query", "fragment", "form_post" ], + "grant_types_supported" : [ "authorization_code", "implicit", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code" ], + "subject_types_supported" : [ "public" ], + "id_token_signing_alg_values_supported" : [ "RS256" ], + "userinfo_signing_alg_values_supported" : [ "none" ], + "request_object_signing_alg_values_supported" : [ "none", "HS256", "HS384", "HS512", "RS256", "RS384", "RS512" ], + "token_endpoint_auth_methods_supported" : [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt" ], + "token_endpoint_auth_signing_alg_values_supported" : [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512" ], + "claim_types_supported" : [ "normal" ], + "claims_supported" : [ "sub", "iss", "auth_time", "acr", "name", "given_name", "family_name", "middle_name", "preferred_username", "profile", "picture", "zoneinfo", "phone_number", "updated_at", "address", "email", "locale" ], + "code_challenge_methods_supported" : [ "plain", "S256" ] +} \ No newline at end of file diff --git a/forgerock-core/src/main/java/org/forgerock/android/auth/KotlinExtensions.kt b/forgerock-core/src/main/java/org/forgerock/android/auth/KotlinExtensions.kt index ce55e886..463a6232 100644 --- a/forgerock-core/src/main/java/org/forgerock/android/auth/KotlinExtensions.kt +++ b/forgerock-core/src/main/java/org/forgerock/android/auth/KotlinExtensions.kt @@ -1,11 +1,13 @@ /* - * Copyright (c) 2023 ForgeRock. All rights reserved. + * Copyright (c) 2023 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. */ package org.forgerock.android.auth +import java.net.URI +import java.net.URISyntaxException import java.text.SimpleDateFormat import java.util.* @@ -18,8 +20,26 @@ infix fun Boolean.then(value: T): T? { return if (this) value else null } +/** + * Convert the Long to Time + * @param pattern The pattern to be used to convert the Long to Time + */ fun Long.convertToTime(pattern: String = "yyyyMMdd HH:mm:ss"): String { val date = Date(this) val simpleDateFormat = SimpleDateFormat(pattern, Locale.getDefault()) return simpleDateFormat.format(date) +} + +/** + * Check if the String is an Absolute URL + */ +fun String?.isAbsoluteUrl(): Boolean { + return try { + this?.let { + val uri = URI(it) + uri.isAbsolute && uri.host != null + } ?: false + } catch (e: URISyntaxException) { + false + } } \ No newline at end of file diff --git a/forgerock-core/src/test/java/org/forgerock/android/auth/ExtensionTest.kt b/forgerock-core/src/test/java/org/forgerock/android/auth/ExtensionTest.kt new file mode 100644 index 00000000..421aa0ec --- /dev/null +++ b/forgerock-core/src/test/java/org/forgerock/android/auth/ExtensionTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 ForgeRock. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +package org.forgerock.android.auth + +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import org.junit.Test + +class ExtensionTest { + @Test + fun testLongDateToString() { + val longTimeStamp: Long = 1672947404167 // 20230105 13:36:44 + val actualResult = longTimeStamp.convertToTime().split(" ") + assertEquals(actualResult[0], "20230105") + assertTrue(actualResult[1].isNotEmpty()) + } + + @Test + fun testLongDateToStringWithDifferentPattern() { + val longTimeStamp: Long = 1672947404167 + val actualResult = longTimeStamp.convertToTime("yyyyMMdd") + val expectedResult = "20230105" + assertEquals(actualResult, expectedResult) + } + + @Test + fun testPoorManTernary() { + val list = listOf(1, 2, 3) + val case1 = (list.contains(1) then true) ?: false + assertTrue(case1) + val case2 = (list.contains(5) then true) ?: false + assertFalse(case2) + } + + @Test + fun testIsAbsoluteUrl() { + val url = "https://www.example.com" + assertTrue(url.isAbsoluteUrl()) + } + + @Test + fun testIsNotAbsoluteUrlWithoutScheme() { + val url = "www.example.com" + assertFalse(url.isAbsoluteUrl()) + } + + @Test + fun testOnlyScheme() { + val url = "https://" // Invalid URL + assertFalse(url.isAbsoluteUrl()) + } + + @Test + fun testOnlySchemeAndPath() { + val url = "https://?value=test" // Invalid URL + assertFalse(url.isAbsoluteUrl()) + } + + @Test + fun testIsNotAbsoluteUrlWithInvalidUrl() { + val url = "/as/revoke" + assertFalse(url.isAbsoluteUrl()) + } + +} \ No newline at end of file diff --git a/samples/app/src/main/java/com/example/app/AppNavHost.kt b/samples/app/src/main/java/com/example/app/AppNavHost.kt index e2439b3b..48c33086 100644 --- a/samples/app/src/main/java/com/example/app/AppNavHost.kt +++ b/samples/app/src/main/java/com/example/app/AppNavHost.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 ForgeRock. All rights reserved. + * Copyright (c) 2023 - 2024 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -46,7 +46,8 @@ fun AppNavHost(navController: NavHostController, startDestination = startDestination) { composable(Destinations.ENV_ROUTE) { - val envViewModel = viewModel() + + val envViewModel: EnvViewModel = viewModel() val preferenceViewModel = viewModel( factory = PreferenceViewModel.factory(LocalContext.current) ) diff --git a/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt b/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt index b978b6e7..f97dfb20 100644 --- a/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt +++ b/samples/app/src/main/java/com/example/app/env/EnvViewModel.kt @@ -12,6 +12,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.forgerock.android.auth.FRAuth import org.forgerock.android.auth.FROptions import org.forgerock.android.auth.FROptionsBuilder @@ -142,6 +146,17 @@ class EnvViewModel : ViewModel() { } } + val pingOidc = FROptionsBuilder.build { + server { + url = "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as" + } + oauth { + oauthClientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1" + oauthRedirectUri = "org.forgerock.demo://oauth2redirect" + oauthScope = "openid email address phone profile revoke" + } + } + var current by mutableStateOf(dbind) private set @@ -152,10 +167,20 @@ class EnvViewModel : ViewModel() { servers.add(local) servers.add(ops) servers.add(forgeblock) + servers.add(pingOidc) } fun select(context: Context, options: FROptions) { - FRAuth.start(context, options) + if(options.server.url.contains("pingone")) { + viewModelScope.launch { + val option = + options.discover(options.server.url + "/.well-known/openid-configuration") + FRAuth.start(context, option) + } + } + else { + FRAuth.start(context, options) + } current = options }