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 5, 2024
1 parent f54dad4 commit 1ce303b
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 52 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
@@ -1,12 +1,21 @@
/*
* 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 kotlinx.coroutines.Dispatchers
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

/**
* Manages SDK configuration information
Expand All @@ -22,7 +31,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 +43,69 @@ 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,

Check warning on line 76 in forgerock-auth/src/main/java/org/forgerock/android/auth/FROptions.kt

View check run for this annotation

Codecov / codecov/patch

forgerock-auth/src/main/java/org/forgerock/android/auth/FROptions.kt#L74-L76

Added lines #L74 - L76 were not covered by tests
response.body?.string()
)
}

val openIdConfiguration = response.body?.string()?.let {
JSONObject(it)
}

val urlPath = UrlPath(
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)
}
} catch (e: Exception) {

Check warning on line 94 in forgerock-auth/src/main/java/org/forgerock/android/auth/FROptions.kt

View check run for this annotation

Codecov / codecov/patch

forgerock-auth/src/main/java/org/forgerock/android/auth/FROptions.kt#L94

Added line #L94 was not covered by tests
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 +180,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 +190,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 @@ -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,9 +384,16 @@ private URL getAuthorizeUrl(Token token, PKCE pkce, String state, Map<String, St
}

URL getAuthorizeUrl() throws MalformedURLException {

String authorizeEndpoint = serverConfig.getAuthorizeEndpoint();

if(isAbsoluteUrl(authorizeEndpoint)) {
return new URL(authorizeEndpoint);

Check warning on line 391 in forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java

View check run for this annotation

Codecov / codecov/patch

forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java#L391

Added line #L391 was not covered by tests
}

Uri.Builder builder = Uri.parse(serverConfig.getUrl()).buildUpon();
if (isNotEmpty(serverConfig.getAuthorizeEndpoint())) {
builder.appendEncodedPath(serverConfig.getAuthorizeEndpoint());
if (isNotEmpty(authorizeEndpoint)) {
builder.appendEncodedPath(authorizeEndpoint);
} else {
builder.appendPath("oauth2")
.appendPath("realms")
Expand All @@ -395,9 +404,16 @@ URL getAuthorizeUrl() throws MalformedURLException {
}

URL getTokenUrl() throws MalformedURLException {

String tokenEndpoint = serverConfig.getTokenEndpoint();

if(isAbsoluteUrl(tokenEndpoint)) {
return new URL(tokenEndpoint);

Check warning on line 411 in forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java

View check run for this annotation

Codecov / codecov/patch

forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java#L411

Added line #L411 was not covered by tests
}

Uri.Builder builder = Uri.parse(serverConfig.getUrl()).buildUpon();
if (isNotEmpty(serverConfig.getTokenEndpoint())) {
builder.appendEncodedPath(serverConfig.getTokenEndpoint());
if (isNotEmpty(tokenEndpoint)) {
builder.appendEncodedPath(tokenEndpoint);
} else {
builder.appendPath("oauth2")
.appendPath("realms")
Expand All @@ -409,9 +425,15 @@ URL getTokenUrl() throws MalformedURLException {

URL getRevokeUrl() throws MalformedURLException {

String revokeEndpoint = serverConfig.getRevokeEndpoint();

if(isAbsoluteUrl(revokeEndpoint)) {
return new URL(revokeEndpoint);

Check warning on line 431 in forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java

View check run for this annotation

Codecov / codecov/patch

forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java#L431

Added line #L431 was not covered by tests
}

Uri.Builder builder = Uri.parse(serverConfig.getUrl()).buildUpon();
if (isNotEmpty(serverConfig.getRevokeEndpoint())) {
builder.appendEncodedPath(serverConfig.getRevokeEndpoint());
if (isNotEmpty(revokeEndpoint)) {
builder.appendEncodedPath(revokeEndpoint);
} else {
builder.appendPath("oauth2")
.appendPath("realms")
Expand All @@ -424,9 +446,15 @@ URL getRevokeUrl() throws MalformedURLException {

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

String sessionEndpoint = serverConfig.getEndSessionEndpoint();

Check warning on line 449 in forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java

View check run for this annotation

Codecov / codecov/patch

forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java#L449

Added line #L449 was not covered by tests

if(isAbsoluteUrl(sessionEndpoint)) {
return new URL(sessionEndpoint);

Check warning on line 452 in forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java

View check run for this annotation

Codecov / codecov/patch

forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java#L452

Added line #L452 was not covered by tests
}

Uri.Builder builder = Uri.parse(serverConfig.getUrl()).buildUpon();
if (isNotEmpty(serverConfig.getEndSessionEndpoint())) {
builder.appendEncodedPath(serverConfig.getEndSessionEndpoint());
if (isNotEmpty(sessionEndpoint)) {
builder.appendEncodedPath(sessionEndpoint);

Check warning on line 457 in forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java

View check run for this annotation

Codecov / codecov/patch

forgerock-auth/src/main/java/org/forgerock/android/auth/OAuth2Client.java#L457

Added line #L457 was not covered by tests
} else {
builder.appendPath("oauth2")
.appendPath("realms")
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 "";

Check warning on line 99 in forgerock-auth/src/main/java/org/forgerock/android/auth/ServerConfig.java

View check run for this annotation

Codecov / codecov/patch

forgerock-auth/src/main/java/org/forgerock/android/auth/ServerConfig.java#L98-L99

Added lines #L98 - L99 were not covered by tests
}
}

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

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);

Check warning on line 93 in forgerock-auth/src/main/java/org/forgerock/android/auth/UserService.java

View check run for this annotation

Codecov / codecov/patch

forgerock-auth/src/main/java/org/forgerock/android/auth/UserService.java#L93

Added line #L93 was not covered by tests
}

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
Expand Up @@ -6,12 +6,18 @@
*/
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.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
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 @@ -58,32 +64,6 @@ class FROptionTest {
assertTrue(option.logger.logLevel == Logger.Level.ERROR)
assertTrue(option.logger.customLogger == logger)
}

@Test(expected = IllegalArgumentException::class)
fun testInValidConfigRealm() {
val option1 = FROptionsBuilder.build { server {
url = "https://stoyan.com"
realm = ""
}}
option1.validateConfig()
}

@Test(expected = IllegalArgumentException::class)
fun testInValidConfigCookieName() {
val option1 = FROptionsBuilder.build { server {
url = "https://stoyan.com"
cookieName = ""
}}
option1.validateConfig()
}

@Test(expected = IllegalArgumentException::class)
fun testInValidConfigUrl() {
val option1 = FROptionsBuilder.build { server {
url = ""
}}
option1.validateConfig()
}
@Test
fun testReferenceAndValue() {
val option1 = FROptionsBuilder.build { server {
Expand All @@ -100,4 +80,27 @@ class FROptionTest {
assertFalse(FROptions.equals(option1, option2))
assertTrue(FROptions.equals(option2, option2))
}

@Test
fun testDiscoverEndpoint() {
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.ca"
}
}
val copied = option1.discover("https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration")
assertTrue(copied.urlPath.authorizeEndpoint == "https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/authorize")
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")
}
}
}
29 changes: 29 additions & 0 deletions forgerock-auth/src/test/resources/discovery.json
Original file line number Diff line number Diff line change
@@ -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" ]
}
Loading

0 comments on commit 1ce303b

Please sign in to comment.