Skip to content

Commit

Permalink
Merge pull request #423 from ForgeRock/SDKS-3022-Discovery
Browse files Browse the repository at this point in the history
OIDC ping discovery
  • Loading branch information
jeyanthanperiyasamy authored Jun 11, 2024
2 parents 2efffe8 + f5bfafd commit 317eef6
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -34,21 +42,83 @@ 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()
.host(url.authority)
.interceptorSupplier {
listOf<Interceptor>(
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
}
}
}

}

/**
* 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 Expand Up @@ -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",
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
/*
* 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.
*/

package org.forgerock.android.auth;


import android.net.Uri;

import lombok.Builder;
import okhttp3.Call;
import okhttp3.OkHttpClient;
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -84,6 +92,7 @@ class FROptionTest {
}}
option1.validateConfig()
}

@Test
fun testReferenceAndValue() {
val option1 = FROptionsBuilder.build { server {
Expand All @@ -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")
}
}


}
Loading

0 comments on commit 317eef6

Please sign in to comment.