Skip to content

Commit

Permalink
OIDC ping discovery
Browse files Browse the repository at this point in the history
  • Loading branch information
jeyanthanperiyasamy committed Jun 4, 2024
1 parent f54dad4 commit ca2a00a
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -34,21 +46,70 @@ 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<Interceptor>(
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
}

}
}

}

/**
* Option builder to build the SDK configuration information
*/
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import android.net.Uri;
import android.util.Base64;
import android.util.Patterns;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -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;

Expand Down Expand Up @@ -382,6 +384,12 @@ private URL getAuthorizeUrl(Token token, PKCE pkce, String state, Map<String, St
}

URL getAuthorizeUrl() throws MalformedURLException {


if(isAbsoluteUrl(serverConfig.getAuthorizeEndpoint())) {
return new URL(serverConfig.getAuthorizeEndpoint());
}

Uri.Builder builder = Uri.parse(serverConfig.getUrl()).buildUpon();
if (isNotEmpty(serverConfig.getAuthorizeEndpoint())) {
builder.appendEncodedPath(serverConfig.getAuthorizeEndpoint());
Expand All @@ -395,6 +403,11 @@ URL getAuthorizeUrl() throws MalformedURLException {
}

URL getTokenUrl() throws MalformedURLException {

if(isAbsoluteUrl(serverConfig.getTokenEndpoint())) {
return new URL(serverConfig.getTokenEndpoint());
}

Uri.Builder builder = Uri.parse(serverConfig.getUrl()).buildUpon();
if (isNotEmpty(serverConfig.getTokenEndpoint())) {
builder.appendEncodedPath(serverConfig.getTokenEndpoint());
Expand All @@ -409,6 +422,10 @@ URL getTokenUrl() throws MalformedURLException {

URL getRevokeUrl() throws MalformedURLException {

if(isAbsoluteUrl(serverConfig.getRevokeEndpoint())) {
return new URL(serverConfig.getRevokeEndpoint());
}

Uri.Builder builder = Uri.parse(serverConfig.getUrl()).buildUpon();
if (isNotEmpty(serverConfig.getRevokeEndpoint())) {
builder.appendEncodedPath(serverConfig.getRevokeEndpoint());
Expand All @@ -424,6 +441,10 @@ URL getRevokeUrl() throws MalformedURLException {

URL getEndSessionUrl(String clientId, String idToken) throws MalformedURLException {

if(isAbsoluteUrl(serverConfig.getEndSessionEndpoint())) {
return new URL(serverConfig.getEndSessionEndpoint());
}

Uri.Builder builder = Uri.parse(serverConfig.getUrl()).buildUpon();
if (isNotEmpty(serverConfig.getEndSessionEndpoint())) {
builder.appendEncodedPath(serverConfig.getEndSessionEndpoint());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ private static String getHost(Context context, String url) {
String u = url == null ? context.getResources().getString(R.string.forgerock_url) : url;
return new URL(u).getHost();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
Logger.error("Invalid URL", e.getMessage());
return "";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

package org.forgerock.android.auth;

import static org.forgerock.android.auth.String_ExtensionKt.isAbsoluteUrl;

import android.net.Uri;
import android.util.Patterns;

import lombok.Builder;
import okhttp3.Call;
import okhttp3.OkHttpClient;
Expand Down Expand Up @@ -84,6 +88,10 @@ public void onResponse(@NotNull Call call, @NotNull Response response) {

private URL getUserInfoUrl() throws MalformedURLException {

if(isAbsoluteUrl(serverConfig.getUserInfoEndpoint())) {
return new URL(serverConfig.getUserInfoEndpoint());
}

Uri.Builder builder = Uri.parse(serverConfig.getUrl()).buildUpon();
if (StringUtils.isNotEmpty(serverConfig.getUserInfoEndpoint())) {
builder.appendEncodedPath(serverConfig.getUserInfoEndpoint());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
*/
package org.forgerock.android.auth

import java.net.URI
import java.net.URISyntaxException
import java.text.SimpleDateFormat
import java.util.*

Expand All @@ -22,4 +24,13 @@ fun Long.convertToTime(pattern: String = "yyyyMMdd HH:mm:ss"): String {
val date = Date(this)
val simpleDateFormat = SimpleDateFormat(pattern, Locale.getDefault())
return simpleDateFormat.format(date)
}

fun String.isAbsoluteUrl(): Boolean {
return try {
val uri = URI(this)
uri.isAbsolute && uri.scheme != null && uri.host != null
} catch (e: URISyntaxException) {
false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 testOnlyScheme() {
val url = "https://" // Invalid URL
assertFalse(url.isAbsoluteUrl())
}

@Test
fun testIsNotAbsoluteUrlWithoutScheme() {
val url = "www.example.com"
assertFalse(url.isAbsoluteUrl())
}

@Test
fun testIsNotAbsoluteUrlWithInvalidUrl() {
val url = "/as/revoke"
assertFalse(url.isAbsoluteUrl())
}

}
11 changes: 11 additions & 0 deletions samples/app/src/main/java/com/example/app/AppDrawer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.example.app.Destinations.CENTRALIZE_PING_ROUTE
import com.example.app.Destinations.CENTRALIZE_ROUTE
import com.example.app.Destinations.DEVICE_PROFILE
import com.example.app.Destinations.ENV_ROUTE
Expand Down Expand Up @@ -96,6 +97,16 @@ fun AppDrawer(
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem(
label = { Text("Centralize Ping Login") },
selected = false,
icon = { Icon(Icons.Filled.OpenInBrowser, null) },
onClick = {
navigateTo(CENTRALIZE_PING_ROUTE);
closeDrawer()
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
NavigationDrawerItem(
label = { Text("WebAuthn Keys") },
selected = false,
Expand Down
5 changes: 5 additions & 0 deletions samples/app/src/main/java/com/example/app/AppNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.example.app.centralize.Centralize
import com.example.app.centralize.CentralizeLoginViewModel
import com.example.app.centralize.CentralizePingLoginViewModel
import com.example.app.device.DeviceProfileRoute
import com.example.app.device.DeviceProfileViewModel
import com.example.app.env.EnvRoute
Expand Down Expand Up @@ -71,6 +72,10 @@ fun AppNavHost(navController: NavHostController,
val centralizeLoginViewModel = viewModel<CentralizeLoginViewModel>()
Centralize(centralizeLoginViewModel)
}
composable(Destinations.CENTRALIZE_PING_ROUTE) {
val centralizePingLoginViewModel = viewModel<CentralizePingLoginViewModel>()
Centralize(centralizePingLoginViewModel)
}
composable(Destinations.MANAGE_WEBAUTHN_KEYS) {
val webAuthnViewModel = viewModel<WebAuthnViewModel>(
factory = WebAuthnViewModel.factory(LocalContext.current)
Expand Down
1 change: 1 addition & 0 deletions samples/app/src/main/java/com/example/app/Destinations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

27 changes: 27 additions & 0 deletions samples/app/src/main/java/com/example/app/centralize/Centralize.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserProfileViewModel>()
UserProfile(userProfileViewModel = userProfileViewModel)
}
state.exception?.apply {
Error(exception = this)
}
}
}

Loading

0 comments on commit ca2a00a

Please sign in to comment.