diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/FRAuth.java b/forgerock-auth/src/main/java/org/forgerock/android/auth/FRAuth.java index 52db439d..795321b3 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/FRAuth.java +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/FRAuth.java @@ -47,8 +47,6 @@ public static synchronized void start(Context context, @Nullable FROptions optio if(!started || !FROptions.equals(cachedOptions, options)) { started = true; FROptions currentOptions = ConfigHelper.load(context, options); - //Validate (AM URL, Realm, CookieName) is not Empty. If its empty will throw IllegalArgumentException. - currentOptions.validateConfig(); if (ConfigHelper.isConfigDifferentFromPersistedValue(context, currentOptions)) { SessionManager sessionManager = ConfigHelper.getPersistedConfig(context, cachedOptions).getSessionManager(); sessionManager.close(); 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 35f7df57..c0ca3421 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,19 @@ */ package org.forgerock.android.auth +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import okhttp3.Interceptor import okhttp3.OkHttpClient +import okhttp3.Request +import org.forgerock.android.auth.OkHttpClientProvider.Companion.getInstance +import org.forgerock.android.auth.exception.ApiException +import org.json.JSONObject +import java.net.URL +import java.util.concurrent.TimeUnit +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException /** * Manages SDK configuration information @@ -22,7 +34,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,13 +46,62 @@ data class FROptions(val server: Server, && old?.logger == new?.logger } } - @Throws(IllegalArgumentException::class) - @JvmName("validateConfig") - internal fun validateConfig() { - require(server.url.isNotBlank()) { "AM URL cannot be blank" } - 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() + .timeUnit(TimeUnit.SECONDS) + .host(url.authority) + .interceptorSupplier { + listOf( + OkHttpRequestInterceptor() + ) + } + .build() + + // Obtain instance of OkHttp client + val httpClient = getInstance().lookup(networkConfig) + return httpClient } + + suspend fun discover(discoverUrl: String): FROptions { + return withContext(Dispatchers.IO) { + try { + val url = URL(discoverUrl) + val httpClient = getOkHttpClient(url) + val request = Request.Builder() + .header(ServerConfig.ACCEPT_API_VERSION, "resource=1.0") + .url(discoverUrl) + .get().build() + + httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw ApiException( + response.code, + response.message, + response.body?.string() + ) + } + + val openIdConfiguration = response.body?.string()?.let { + JSONObject(it) + } + + val urlPath = UrlPath( + openIdConfiguration?.getString("authorization_endpoint"), + openIdConfiguration?.getString("token_endpoint"), + openIdConfiguration?.getString("userinfo_endpoint"), + openIdConfiguration?.getString("revocation_endpoint"), + openIdConfiguration?.getString("end_session_endpoint")) + + + this@FROptions.copy(urlPath = urlPath) + } + } catch (e: Exception) { + throw e + } + + } + } + } /** @@ -48,7 +109,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() diff --git a/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java b/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java index 8cc4b039..ab82e15f 100644 --- a/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java +++ b/forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java @@ -9,6 +9,7 @@ import android.net.Uri; import android.util.Base64; +import android.util.Patterns; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -33,6 +34,7 @@ import okhttp3.RequestBody; import okhttp3.Response; +import static org.forgerock.android.auth.KotlinExtensionsKt.isAbsoluteUrl; import static org.forgerock.android.auth.ServerConfig.ACCEPT_API_VERSION; import static org.forgerock.android.auth.StringUtils.isNotEmpty; @@ -382,6 +384,12 @@ private URL getAuthorizeUrl(Token token, PKCE pkce, String state, Map() Centralize(centralizeLoginViewModel) } + composable(Destinations.CENTRALIZE_PING_ROUTE) { + val centralizePingLoginViewModel = viewModel() + Centralize(centralizePingLoginViewModel) + } composable(Destinations.MANAGE_WEBAUTHN_KEYS) { val webAuthnViewModel = viewModel( factory = WebAuthnViewModel.factory(LocalContext.current) diff --git a/samples/app/src/main/java/com/example/app/Destinations.kt b/samples/app/src/main/java/com/example/app/Destinations.kt index 910b1a34..3ad9acf8 100644 --- a/samples/app/src/main/java/com/example/app/Destinations.kt +++ b/samples/app/src/main/java/com/example/app/Destinations.kt @@ -18,6 +18,7 @@ object Destinations { const val DEVICE_PROFILE = "Device Profile" const val SETTING = "Setting" const val CENTRALIZE_ROUTE = "Centralize Login" + const val CENTRALIZE_PING_ROUTE = "Centralize Ping Login" const val USER_SESSION = "User Session" } diff --git a/samples/app/src/main/java/com/example/app/centralize/Centralize.kt b/samples/app/src/main/java/com/example/app/centralize/Centralize.kt index 716f9c1a..36a21be9 100644 --- a/samples/app/src/main/java/com/example/app/centralize/Centralize.kt +++ b/samples/app/src/main/java/com/example/app/centralize/Centralize.kt @@ -50,3 +50,30 @@ fun Centralize(centralizeLoginViewModel: CentralizeLoginViewModel) { } } +@Composable +fun Centralize(centralizeLoginViewModel: CentralizePingLoginViewModel) { + + val activity = LocalContext.current as FragmentActivity + + LaunchedEffect(true) { + //Not relaunch when recomposition + centralizeLoginViewModel.login(activity) + } + + val state by centralizeLoginViewModel.state.collectAsState() + + + Column(modifier = Modifier + .padding(16.dp) + .fillMaxWidth()) { + state.user?.apply { + val userProfileViewModel = + viewModel() + UserProfile(userProfileViewModel = userProfileViewModel) + } + state.exception?.apply { + Error(exception = this) + } + } +} + diff --git a/samples/app/src/main/java/com/example/app/centralize/CentrallizePingLoginViewModel.kt b/samples/app/src/main/java/com/example/app/centralize/CentrallizePingLoginViewModel.kt new file mode 100644 index 00000000..fcbd2028 --- /dev/null +++ b/samples/app/src/main/java/com/example/app/centralize/CentrallizePingLoginViewModel.kt @@ -0,0 +1,60 @@ +/* + * 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 com.example.app.centralize + +import androidx.browser.customtabs.CustomTabsIntent +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.forgerock.android.auth.FRAuth +import org.forgerock.android.auth.FRListener +import org.forgerock.android.auth.FROptionsBuilder +import org.forgerock.android.auth.FRUser + +class CentralizePingLoginViewModel : ViewModel() { + + var state = MutableStateFlow(CentralizeState()) + private set + fun login(fragmentActivity: FragmentActivity) { + viewModelScope.launch { + val froptions = FROptionsBuilder.build { + + oauth { + oauthClientId = "c12743f9-08e8-4420-a624-71bbb08e9fe1" + oauthRedirectUri = "org.forgerock.demo://oauth2redirect" + oauthScope = "openid email address phone profile" + } + } + val options = froptions.discover("https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration") + FRAuth.start(fragmentActivity, options) + + FRUser.browser().appAuthConfigurer().customTabsIntent { + it.setColorScheme(CustomTabsIntent.COLOR_SCHEME_DARK) + + }.done() + .login(fragmentActivity, + object : FRListener { + override fun onSuccess(result: FRUser) { + state.update { + it.copy(user = result, exception = null) + } + } + + override fun onException(e: Exception) { + state.update { + it.copy(user = null, exception = e) + } + } + }) + } + } + +} \ No newline at end of file