From c7fc20d929be0783093cc65738e5dd21b6736cf0 Mon Sep 17 00:00:00 2001 From: Amanpal Singh <87360222+aman-alfresco@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:15:26 +0530 Subject: [PATCH 1/3] added auth0 authentication --- auth/build.gradle | 4 +- .../main/java/com/alfresco/auth/AuthConfig.kt | 7 +- .../java/com/alfresco/auth/AuthInterceptor.kt | 12 + .../main/java/com/alfresco/auth/AuthType.kt | 5 + .../com/alfresco/auth/DiscoveryService.kt | 14 +- .../alfresco/auth/data/AppConfigDetails.kt | 42 ++++ .../com/alfresco/auth/pkce/PkceAuthService.kt | 156 ++++++++++-- .../auth/ui/AuthenticationActivity.kt | 17 ++ auth/src/main/res/values/strings.xml | 6 + .../com/alfresco/content/apis/AppConfigApi.kt | 2 +- .../alfresco/content/models/AppConfigModel.kt | 227 ----------------- .../com/alfresco/content/models/SearchItem.kt | 232 ++++++++++++++++++ gradle/libs.versions.toml | 10 + sample/build.gradle | 1 + .../java/com/alfresco/sample/AuthConfig.kt | 12 +- .../java/com/alfresco/sample/LoginActivity.kt | 4 +- .../java/com/alfresco/sample/MainViewModel.kt | 4 +- settings.gradle | 2 +- 18 files changed, 502 insertions(+), 255 deletions(-) create mode 100644 auth/src/main/java/com/alfresco/auth/data/AppConfigDetails.kt create mode 100644 auth/src/main/res/values/strings.xml create mode 100644 content/src/main/kotlin/com/alfresco/content/models/SearchItem.kt diff --git a/auth/build.gradle b/auth/build.gradle index e8bf564..14b1d1e 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -6,14 +6,14 @@ apply plugin: 'org.jetbrains.dokka' android { namespace 'com.alfresco.auth' defaultConfig { - versionName "0.8.1" + versionName "0.8.2" } } dependencies { implementation libs.appauth implementation libs.jwtdecode - + implementation libs.auth0 implementation libs.kotlin.serialization.json implementation libs.androidx.appcompat diff --git a/auth/src/main/java/com/alfresco/auth/AuthConfig.kt b/auth/src/main/java/com/alfresco/auth/AuthConfig.kt index 463c687..2e338c2 100644 --- a/auth/src/main/java/com/alfresco/auth/AuthConfig.kt +++ b/auth/src/main/java/com/alfresco/auth/AuthConfig.kt @@ -38,7 +38,12 @@ data class AuthConfig( /** * Path to content service */ - var contentServicePath: String + var contentServicePath: String, + + /** + * Path to content service + */ + var scheme: String = "" ) { /** * Convenience method for JSON serialization. diff --git a/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt b/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt index ca3631b..11b388d 100644 --- a/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt +++ b/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt @@ -40,6 +40,7 @@ class AuthInterceptor( provider = when (authType) { AuthType.BASIC -> BasicProvider(stateString) AuthType.PKCE -> PkceProvider(stateString, config) + AuthType.OIDC -> OIDCProvider(stateString) AuthType.UNKNOWN -> PlainProvider() } } @@ -224,10 +225,21 @@ class AuthInterceptor( } } + private inner class OIDCProvider (val accessToken : String) : Provider{ + override fun intercept(chain: Interceptor.Chain): Response { + return chain.proceed(AuthType.OIDC, accessToken) + } + + override fun finish() { + localScope.coroutineContext.cancelChildren() + } + } + private fun Interceptor.Chain.proceed(type: AuthType, token: String?): Response { val headerValue = when (type) { AuthType.BASIC -> "Basic $token" AuthType.PKCE -> "Bearer $token" + AuthType.OIDC -> "bearer $token" AuthType.UNKNOWN -> null } return proceedWithAuthorization(headerValue) diff --git a/auth/src/main/java/com/alfresco/auth/AuthType.kt b/auth/src/main/java/com/alfresco/auth/AuthType.kt index b033db7..4e5d75c 100644 --- a/auth/src/main/java/com/alfresco/auth/AuthType.kt +++ b/auth/src/main/java/com/alfresco/auth/AuthType.kt @@ -15,6 +15,11 @@ enum class AuthType(val value: String) { */ PKCE("pkce"), + /** + * Used to specify the need of SSO auth + */ + OIDC("oidc"), + /** * Used to specify that the auth type is unknown */ diff --git a/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt b/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt index 7912713..906e170 100644 --- a/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt +++ b/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt @@ -5,12 +5,12 @@ import android.net.Uri import com.alfresco.auth.data.ContentServerDetails import com.alfresco.auth.data.ContentServerDetailsData import com.alfresco.auth.pkce.PkceAuthService -import java.net.URL -import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request +import java.net.URL +import java.util.concurrent.TimeUnit /** * Class that facilitates service discovery process. @@ -28,6 +28,8 @@ class DiscoveryService( isPkceType(endpoint) -> AuthType.PKCE + isOIDC(endpoint) -> AuthType.OIDC + isBasicType(endpoint) -> AuthType.BASIC else -> AuthType.UNKNOWN @@ -97,10 +99,16 @@ class DiscoveryService( val result = try { val authService = PkceAuthService(context, null, authConfig) authService.fetchDiscoveryFromUrl(uri) - } catch (exception: Exception) { null } + } catch (exception: Exception) { + null + } return result != null } + private fun isOIDC(endpoint: String): Boolean { + return authConfig.realm.isBlank() + } + /** * Return content service url based on [endpoint]. */ diff --git a/auth/src/main/java/com/alfresco/auth/data/AppConfigDetails.kt b/auth/src/main/java/com/alfresco/auth/data/AppConfigDetails.kt new file mode 100644 index 0000000..cc6225f --- /dev/null +++ b/auth/src/main/java/com/alfresco/auth/data/AppConfigDetails.kt @@ -0,0 +1,42 @@ +package com.alfresco.auth.data + +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json + +@Serializable +data class OAuth2Data( + val host: String, + val clientId: String, + val secret: String, + val scope: String, + val implicitFlow: Boolean, + val codeFlow: Boolean, + val silentLogin: Boolean, + val publicUrls: List, + val redirectSilentIframeUri: String, + val redirectUri: String, + val logoutUrl: String, + val logoutParameters: List, + val redirectUriLogout: String, + val audience: String, + val skipIssuerCheck: Boolean, + val strictDiscoveryDocumentValidation: Boolean +) + +@Serializable +internal data class AppConfigDetails( + val oauth2: OAuth2Data +) { + companion object { + private val json = Json { ignoreUnknownKeys = true } + + fun jsonDeserialize(str: String): AppConfigDetails? { + return try { + json.decodeFromString(serializer(), str) + } catch (ex: SerializationException) { + null + } + } + } +} diff --git a/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt b/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt index 9a43067..08046fd 100644 --- a/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt +++ b/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt @@ -4,11 +4,18 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri +import android.util.Log import com.alfresco.auth.AuthConfig +import com.alfresco.auth.R +import com.alfresco.auth.data.AppConfigDetails +import com.alfresco.auth.data.OAuth2Data +import com.alfresco.auth.ui.AuthenticationActivity +import com.auth0.android.Auth0 +import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.callback.Callback import com.auth0.android.jwt.JWT -import java.util.Locale -import java.util.concurrent.atomic.AtomicReference -import kotlin.coroutines.resumeWithException +import com.auth0.android.provider.WebAuthProvider +import com.auth0.android.result.Credentials import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext @@ -24,6 +31,13 @@ import net.openid.appauth.ResponseTypeValues import net.openid.appauth.TokenResponse import net.openid.appauth.browser.AnyBrowserMatcher import net.openid.appauth.connectivity.ConnectionBuilder +import okhttp3.OkHttpClient +import okhttp3.Request +import java.net.URL +import java.util.Locale +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import kotlin.coroutines.resumeWithException internal class PkceAuthService(context: Context, authState: AuthState?, authConfig: AuthConfig) { @@ -64,9 +78,11 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf serviceConfiguration != null -> { it.resumeWith(Result.success(serviceConfiguration)) } + ex != null -> { it.resumeWithException(ex) } + else -> it.resumeWithException(Exception()) } }, @@ -74,6 +90,31 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf ) } + suspend fun getAppConfigOAuth2Details(appConfigURL: String): OAuth2Data? { + + return withContext(Dispatchers.IO) { + try { + val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .build() + val request = Request.Builder() + .url(URL(appConfigURL)) + .get() + .build() + val response = client.newCall(request).execute() + + if (response.code != 200) return@withContext null + + val body = response.body?.string() ?: "" + val data = AppConfigDetails.jsonDeserialize(body) + data?.oauth2 + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } + /** * Initiates the login in [activity] with activity result [requestCode] */ @@ -82,27 +123,102 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf checkConfig(authConfig) // build discovery url using auth configuration - val discoveryUri = discoveryUriWith(endpoint, authConfig) - withContext(Dispatchers.IO) { - val config = fetchDiscoveryFromUrl(discoveryUri) + if (authConfig.realm.isBlank()) { + val uri = discoveryUriWithAuth0(endpoint).toString() + withContext(Dispatchers.IO) { - // save the authorization configuration - authState.set(AuthState(config)) + val authDetails = getAppConfigOAuth2Details(uri) - val authRequest = generateAuthorizationRequest(config) - val authIntent = generateAuthIntent(authRequest) + authDetails?.let { oauth2 -> + println("PkceAuthService.initiateLogin $authDetails") - withContext(Dispatchers.Main) { - activity.startActivityForResult(authIntent, requestCode) + val credentials = webAuthAsync( + authConfig, + oauth2, + activity + ) + + withContext(Dispatchers.Main) { + (activity as AuthenticationActivity<*>).handleResult( + credentials, + authConfig + ) + } + } + + } +// discoveryUriWithAuth0() + } else { + val discoveryUri = discoveryUriWith(endpoint, authConfig) + + withContext(Dispatchers.IO) { + val config = fetchDiscoveryFromUrl(discoveryUri) + + // save the authorization configuration + authState.set(AuthState(config)) + + val authRequest = generateAuthorizationRequest(config) + val authIntent = generateAuthIntent(authRequest) + + withContext(Dispatchers.Main) { + activity.startActivityForResult(authIntent, requestCode) + } } } } + private suspend fun loginWithBrowser( + account: Auth0, + authConfig: AuthConfig, + endpoint: String, + activity: Activity + ) { + // Setup the WebAuthProvider, using the custom scheme and scope. + WebAuthProvider.login(account) + .withScheme(authConfig.scheme) + .withScope("openid profile email read:current_user update:current_user_metadata") + .withAudience("https://${endpoint}/api/v2/") + + // Launch the authentication passing the callback where the results will be received + .start(activity, object : Callback { + override fun onFailure(exception: AuthenticationException) { + Log.d("Test OIDC 3 :: ", "Failure: ${exception.getCode()}") + } + + override fun onSuccess(credentials: Credentials) { + Log.d("Test OIDC 4 :: ", "Success: ${credentials.accessToken}") + } + }) + } + + private suspend fun webAuthAsync( + authConfig: AuthConfig, + oauth2: OAuth2Data, + activity: Activity + ): Credentials? { + return try { + val account = Auth0(oauth2.clientId, oauth2.host) + val credentials = WebAuthProvider.login(account) + .withScheme(authConfig.scheme) + .withAudience(oauth2.audience) + .withScope("openid profile email") + .await(activity) + Log.d("Test OIDC 4 :: ", "Success: ${credentials.accessToken}") + credentials + } catch (error: AuthenticationException) { + val message = + if (error.isCanceled) "Browser was closed" else error.getDescription() + Log.d("Test OIDC 3 :: ", "Failure: $message") + null + } + } + fun initiateReLogin(activity: Activity, requestCode: Int) { requireNotNull(authState.get()) - val authRequest = generateAuthorizationRequest(authState.get().authorizationServiceConfiguration!!) + val authRequest = + generateAuthorizationRequest(authState.get().authorizationServiceConfiguration!!) val authIntent = generateAuthIntent(authRequest) activity.startActivityForResult(authIntent, requestCode) @@ -154,9 +270,11 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf response != null -> { it.resumeWith(Result.success(authState.get().jsonSerializeString())) } + ex != null -> { it.resumeWithException(ex) } + else -> it.resumeWithException(Exception()) } } @@ -177,9 +295,11 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf response != null -> { it.resumeWith(Result.success(response)) } + ex != null -> { it.resumeWithException(ex) } + else -> it.resumeWithException(Exception()) } } @@ -251,7 +371,7 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf */ private fun checkConfig(authConfig: AuthConfig) { require(authConfig.contentServicePath.isNotBlank()) { "Content service path is blank or empty" } - require(authConfig.realm.isNotBlank()) { "Realm is blank or empty" } +// require(authConfig.realm.isNotBlank()) { "Realm is blank or empty" } require(authConfig.clientId.isNotBlank()) { "Client id is blank or empty" } require(authConfig.redirectUrl.isNotBlank()) { "Redirect url is blank or empty" } } @@ -288,7 +408,7 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf * If the [endpoint] contains either schema or port that will override [config] information. */ fun endpointWith(endpoint: String, config: AuthConfig): Uri { - val src = endpoint.trim().toLowerCase(Locale.ROOT) + val src = endpoint.trim().lowercase(Locale.ROOT) var uri = Uri.parse(src) var uriBuilder = uri.buildUpon() @@ -321,5 +441,11 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf .appendPath(OPENID_CONFIGURATION_RESOURCE) .build() } + + fun discoveryUriWithAuth0(endpoint: String): Uri { + return Uri.parse("https://${endpoint}/app.config.json") + .buildUpon() + .build() + } } } diff --git a/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt b/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt index 78b177b..80e8ab7 100644 --- a/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt +++ b/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt @@ -86,6 +86,7 @@ abstract class AuthenticationViewModel : ViewModel() { open fun onPkceAuthCancelled() {} internal val pkceAuth = PkceAuth() + internal inner class PkceAuth { private lateinit var authService: PkceAuthService @@ -127,6 +128,18 @@ abstract class AuthenticationViewModel : ViewModel() { } } } + + fun handleActivityResult(credentials: com.auth0.android.result.Credentials) { + viewModelScope.launch { + try { + val result = credentials.accessToken + val userEmail = credentials.user.email ?: "" + _onCredentials.value = Credentials(userEmail, result, AuthType.PKCE.value) + } catch (ex: Exception) { + _onError.value = ex.message ?: "" + } + } + } } } @@ -166,6 +179,10 @@ abstract class AuthenticationActivity : AppCompatAc super.onActivityResult(requestCode, resultCode, data) } + fun handleResult(data: com.auth0.android.result.Credentials?, authConfig: AuthConfig) { + data?.let { viewModel.pkceAuth.handleActivityResult(it) } + } + /** * Called when [credentials] become available. */ diff --git a/auth/src/main/res/values/strings.xml b/auth/src/main/res/values/strings.xml new file mode 100644 index 0000000..a6b6314 --- /dev/null +++ b/auth/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + zTSfC3VEcrguvP5CKIgNIBCOTliawIKg + *.auth0.com + demo + diff --git a/content/src/main/kotlin/com/alfresco/content/apis/AppConfigApi.kt b/content/src/main/kotlin/com/alfresco/content/apis/AppConfigApi.kt index f079e02..fba9ddf 100644 --- a/content/src/main/kotlin/com/alfresco/content/apis/AppConfigApi.kt +++ b/content/src/main/kotlin/com/alfresco/content/apis/AppConfigApi.kt @@ -16,6 +16,6 @@ interface AppConfigApi { "Content-Type: application/json" ) @GET("app.config.json") - suspend fun getAppConfig() :AppConfigModel + suspend fun getAppConfig(): AppConfigModel } diff --git a/content/src/main/kotlin/com/alfresco/content/models/AppConfigModel.kt b/content/src/main/kotlin/com/alfresco/content/models/AppConfigModel.kt index e953ca6..dd60502 100644 --- a/content/src/main/kotlin/com/alfresco/content/models/AppConfigModel.kt +++ b/content/src/main/kotlin/com/alfresco/content/models/AppConfigModel.kt @@ -14,230 +14,3 @@ import kotlinx.parcelize.Parcelize data class AppConfigModel( @Json(name = "search") @field:Json(name = "search") val search: List? ) : Parcelable - -/** - * Categories model - * @property expanded - * @property component - * @property name - * @property id - * @property enabled - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class CategoriesItem( - @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, - @Json(name = "component") @field:Json(name = "component") val component: Component?, - @Json(name = "name") @field:Json(name = "name") val name: String?, - @Json(name = "id") @field:Json(name = "id") val id: String?, - @Json(name = "enabled") @field:Json(name = "enabled") val enabled: Boolean? -) : Parcelable - -/** - * Component model - * @property settings - * @property selector - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class Component( - @Json(name = "settings") @field:Json(name = "settings") val settings: Settings?, - @Json(name = "selector") @field:Json(name = "selector") val selector: String? -) : Parcelable - -/** - * SearchItem model - * @property default - * @property name - * @property filterWithContains - * @property categories - * @property resetButton - * @property facetFields - * @property facetQueries - * @property facetIntervals - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class SearchItem( - @Json(name = "default") @field:Json(name = "default") val default: Boolean?, - @Json(name = "name") @field:Json(name = "name") val name: String?, - @Json(name = "filterWithContains") @field:Json(name = "filterWithContains") val filterWithContains: Boolean?, - @Json(name = "categories") @field:Json(name = "categories") val categories: List?, - @Json(name = "resetButton") @field:Json(name = "resetButton") val resetButton: Boolean?, - @Json(name = "filterQueries") @field:Json(name = "filterQueries") val filterQueries: List?, - @Json(name = "facetFields") @field:Json(name = "facetFields") val facetFields: FacetFieldsItem?, - @Json(name = "facetQueries") @field:Json(name = "facetQueries") val facetQueries: FacetQueriesItem?, - @Json(name = "facetIntervals") @field:Json(name = "facetIntervals") val facetIntervals: FacetIntervalsItem? -) : Parcelable - -/** - * Filter Queries Model - * @property query - * @property fields - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class FilterQueriesItem( - @Json(name = "query") @field:Json(name = "query") val query: String? -) : Parcelable - -/** - * Facet Fields Model - * @property expanded - * @property fields - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class FacetFieldsItem( - @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, - @Json(name = "fields") @field:Json(name = "fields") val fields: List? -) : Parcelable - -/** - * Facet Queries Model - * @property label - * @property pageSize - * @property expanded - * @property mincount - * @property queries - * @property settings - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class FacetQueriesItem( - @Json(name = "label") @field:Json(name = "label") val label: String?, - @Json(name = "pageSize") @field:Json(name = "pageSize") val pageSize: Int?, - @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, - @Json(name = "mincount") @field:Json(name = "mincount") val mincount: Int?, - @Json(name = "queries") @field:Json(name = "queries") val queries: List?, - @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? -) : Parcelable - -/** - * Facet Fields Model - * @property expanded - * @property intervals - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class FacetIntervalsItem( - @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, - @Json(name = "intervals") @field:Json(name = "intervals") val intervals: List? -) : Parcelable - -/** - * Fields Model - * @property field - * @property mincount - * @property label - * @property settings - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class FieldsItem( - @Json(name = "field") @field:Json(name = "field") val field: String?, - @Json(name = "mincount") @field:Json(name = "mincount") val mincount: Int?, - @Json(name = "label") @field:Json(name = "label") val label: String?, - @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? -) : Parcelable - -/** - * Queries Model - * @property query - * @property label - * @property group - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class QueriesItem( - @Json(name = "query") @field:Json(name = "query") val query: String?, - @Json(name = "label") @field:Json(name = "label") val label: String?, - @Json(name = "group") @field:Json(name = "group") val group: String? -) : Parcelable - -/** - * Queries Model - * @property label - * @property field - * @property sets - * @property settings - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class IntervalsItem( - @Json(name = "label") @field:Json(name = "label") val label: String?, - @Json(name = "field") @field:Json(name = "field") val field: String?, - @Json(name = "sets") @field:Json(name = "sets") val sets: List?, - @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? -) : Parcelable - -/** - * Queries Model - * @property label - * @property start - * @property end - * @property endInclusive - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class SetsItem( - @Json(name = "label") @field:Json(name = "label") val label: String?, - @Json(name = "start") @field:Json(name = "start") val start: String?, - @Json(name = "end") @field:Json(name = "end") val end: String?, - @Json(name = "startInclusive") @field:Json(name = "startInclusive") val startInclusive: Boolean?, - @Json(name = "endInclusive") @field:Json(name = "endInclusive") val endInclusive: Boolean? -) : Parcelable - -/** - * Settings model - * @property field - * @property pattern - * @property placeholder - * @property pageSize - * @property operator - * @property options - * @property min - * @property max - * @property step - * @property thumbLabel - * @property format - * @property dateFormat - * @property maxDate - * @property allowUpdateOnChange - * @property hideDefaultAction - * @property unit - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class Settings( - @Json(name = "field") @field:Json(name = "field") val field: String?, - @Json(name = "pattern") @field:Json(name = "pattern") val pattern: String?, - @Json(name = "placeholder") @field:Json(name = "placeholder") val placeholder: String?, - @Json(name = "pageSize") @field:Json(name = "pageSize") val pageSize: Int?, - @Json(name = "operator") @field:Json(name = "operator") val operator: String?, - @Json(name = "options") @field:Json(name = "options") val options: List?, - @Json(name = "min") @field:Json(name = "min") val min: Int?, - @Json(name = "max") @field:Json(name = "max") val max: Int?, - @Json(name = "step") @field:Json(name = "step") val step: Int?, - @Json(name = "thumbLabel") @field:Json(name = "thumbLabel") val thumbLabel: Boolean?, - @Json(name = "format") @field:Json(name = "format") val format: String?, - @Json(name = "dateFormat") @field:Json(name = "dateFormat") val dateFormat: String?, - @Json(name = "maxDate") @field:Json(name = "maxDate") val maxDate: String?, - @Json(name = "allowUpdateOnChange") @field:Json(name = "allowUpdateOnChange") val allowUpdateOnChange: Boolean?, - @Json(name = "hideDefaultAction") @field:Json(name = "hideDefaultAction") val hideDefaultAction: Boolean?, - @Json(name = "unit") @field:Json(name = "unit") val unit: String? -) : Parcelable - -/** - * Options model - * @property name - * @property value - * @property default - */ -@Parcelize -@JsonClass(generateAdapter = true) -data class Options( - @Json(name = "name") @field:Json(name = "name") val name: String?, - @Json(name = "value") @field:Json(name = "value") val value: String?, - @Json(name = "default") @field:Json(name = "default") val default: Boolean? -) : Parcelable diff --git a/content/src/main/kotlin/com/alfresco/content/models/SearchItem.kt b/content/src/main/kotlin/com/alfresco/content/models/SearchItem.kt new file mode 100644 index 0000000..0bbaa20 --- /dev/null +++ b/content/src/main/kotlin/com/alfresco/content/models/SearchItem.kt @@ -0,0 +1,232 @@ +package com.alfresco.content.models + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize +/** + * Categories model + * @property expanded + * @property component + * @property name + * @property id + * @property enabled + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class CategoriesItem( + @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, + @Json(name = "component") @field:Json(name = "component") val component: Component?, + @Json(name = "name") @field:Json(name = "name") val name: String?, + @Json(name = "id") @field:Json(name = "id") val id: String?, + @Json(name = "enabled") @field:Json(name = "enabled") val enabled: Boolean? +) : Parcelable + +/** + * Component model + * @property settings + * @property selector + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class Component( + @Json(name = "settings") @field:Json(name = "settings") val settings: Settings?, + @Json(name = "selector") @field:Json(name = "selector") val selector: String? +) : Parcelable + +/** + * SearchItem model + * @property default + * @property name + * @property filterWithContains + * @property categories + * @property resetButton + * @property facetFields + * @property facetQueries + * @property facetIntervals + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class SearchItem( + @Json(name = "default") @field:Json(name = "default") val default: Boolean?, + @Json(name = "name") @field:Json(name = "name") val name: String?, + @Json(name = "filterWithContains") @field:Json(name = "filterWithContains") val filterWithContains: Boolean?, + @Json(name = "categories") @field:Json(name = "categories") val categories: List?, + @Json(name = "resetButton") @field:Json(name = "resetButton") val resetButton: Boolean?, + @Json(name = "filterQueries") @field:Json(name = "filterQueries") val filterQueries: List?, + @Json(name = "facetFields") @field:Json(name = "facetFields") val facetFields: FacetFieldsItem?, + @Json(name = "facetQueries") @field:Json(name = "facetQueries") val facetQueries: FacetQueriesItem?, + @Json(name = "facetIntervals") @field:Json(name = "facetIntervals") val facetIntervals: FacetIntervalsItem? +) : Parcelable + +/** + * Filter Queries Model + * @property query + * @property fields + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class FilterQueriesItem( + @Json(name = "query") @field:Json(name = "query") val query: String? +) : Parcelable + +/** + * Facet Fields Model + * @property expanded + * @property fields + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class FacetFieldsItem( + @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, + @Json(name = "fields") @field:Json(name = "fields") val fields: List? +) : Parcelable + +/** + * Facet Queries Model + * @property label + * @property pageSize + * @property expanded + * @property mincount + * @property queries + * @property settings + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class FacetQueriesItem( + @Json(name = "label") @field:Json(name = "label") val label: String?, + @Json(name = "pageSize") @field:Json(name = "pageSize") val pageSize: Int?, + @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, + @Json(name = "mincount") @field:Json(name = "mincount") val mincount: Int?, + @Json(name = "queries") @field:Json(name = "queries") val queries: List?, + @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? +) : Parcelable + +/** + * Facet Fields Model + * @property expanded + * @property intervals + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class FacetIntervalsItem( + @Json(name = "expanded") @field:Json(name = "expanded") val expanded: Boolean?, + @Json(name = "intervals") @field:Json(name = "intervals") val intervals: List? +) : Parcelable + +/** + * Fields Model + * @property field + * @property mincount + * @property label + * @property settings + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class FieldsItem( + @Json(name = "field") @field:Json(name = "field") val field: String?, + @Json(name = "mincount") @field:Json(name = "mincount") val mincount: Int?, + @Json(name = "label") @field:Json(name = "label") val label: String?, + @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? +) : Parcelable + +/** + * Queries Model + * @property query + * @property label + * @property group + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class QueriesItem( + @Json(name = "query") @field:Json(name = "query") val query: String?, + @Json(name = "label") @field:Json(name = "label") val label: String?, + @Json(name = "group") @field:Json(name = "group") val group: String? +) : Parcelable + +/** + * Queries Model + * @property label + * @property field + * @property sets + * @property settings + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class IntervalsItem( + @Json(name = "label") @field:Json(name = "label") val label: String?, + @Json(name = "field") @field:Json(name = "field") val field: String?, + @Json(name = "sets") @field:Json(name = "sets") val sets: List?, + @Json(name = "settings") @field:Json(name = "settings") val settings: Settings? +) : Parcelable + +/** + * Queries Model + * @property label + * @property start + * @property end + * @property endInclusive + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class SetsItem( + @Json(name = "label") @field:Json(name = "label") val label: String?, + @Json(name = "start") @field:Json(name = "start") val start: String?, + @Json(name = "end") @field:Json(name = "end") val end: String?, + @Json(name = "startInclusive") @field:Json(name = "startInclusive") val startInclusive: Boolean?, + @Json(name = "endInclusive") @field:Json(name = "endInclusive") val endInclusive: Boolean? +) : Parcelable + +/** + * Settings model + * @property field + * @property pattern + * @property placeholder + * @property pageSize + * @property operator + * @property options + * @property min + * @property max + * @property step + * @property thumbLabel + * @property format + * @property dateFormat + * @property maxDate + * @property allowUpdateOnChange + * @property hideDefaultAction + * @property unit + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class Settings( + @Json(name = "field") @field:Json(name = "field") val field: String?, + @Json(name = "pattern") @field:Json(name = "pattern") val pattern: String?, + @Json(name = "placeholder") @field:Json(name = "placeholder") val placeholder: String?, + @Json(name = "pageSize") @field:Json(name = "pageSize") val pageSize: Int?, + @Json(name = "operator") @field:Json(name = "operator") val operator: String?, + @Json(name = "options") @field:Json(name = "options") val options: List?, + @Json(name = "min") @field:Json(name = "min") val min: Int?, + @Json(name = "max") @field:Json(name = "max") val max: Int?, + @Json(name = "step") @field:Json(name = "step") val step: Int?, + @Json(name = "thumbLabel") @field:Json(name = "thumbLabel") val thumbLabel: Boolean?, + @Json(name = "format") @field:Json(name = "format") val format: String?, + @Json(name = "dateFormat") @field:Json(name = "dateFormat") val dateFormat: String?, + @Json(name = "maxDate") @field:Json(name = "maxDate") val maxDate: String?, + @Json(name = "allowUpdateOnChange") @field:Json(name = "allowUpdateOnChange") val allowUpdateOnChange: Boolean?, + @Json(name = "hideDefaultAction") @field:Json(name = "hideDefaultAction") val hideDefaultAction: Boolean?, + @Json(name = "unit") @field:Json(name = "unit") val unit: String? +) : Parcelable + +/** + * Options model + * @property name + * @property value + * @property default + */ +@Parcelize +@JsonClass(generateAdapter = true) +data class Options( + @Json(name = "name") @field:Json(name = "name") val name: String?, + @Json(name = "value") @field:Json(name = "value") val value: String?, + @Json(name = "default") @field:Json(name = "default") val default: Boolean? +) : Parcelable diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b35a73f..22fd57c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,12 @@ [versions] +auth0 = "2.10.2" kotlin = "1.8.0" retrofit = "2.9.0" moshi = "1.15.0" +coreKtx = "1.13.1" +junit = "1.1.5" +espressoCore = "3.5.1" +material = "1.12.0" [libraries] android-desugar = "com.android.tools:desugar_jdk_libs:2.0.3" @@ -13,6 +18,7 @@ androidx-lifecycle-viewmodelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:2. appauth = "net.openid:appauth:0.11.1" +auth0 = { module = "com.auth0.android:auth0", version.ref = "auth0" } dokka = "org.jetbrains.dokka:dokka-gradle-plugin:1.8.10" gradleVersionsPlugin = "com.github.ben-manes:gradle-versions-plugin:0.46.0" @@ -40,3 +46,7 @@ retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "ret spotless = "com.diffplug.spotless:spotless-plugin-gradle:6.18.0" swaggerCodegen = "com.agologan:swagger-gradle-codegen:1.4.1-67-gfbda3ab" +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junit" } +androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } +material = { group = "com.google.android.material", name = "material", version.ref = "material" } diff --git a/sample/build.gradle b/sample/build.gradle index 205b076..92ea767 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -22,6 +22,7 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + manifestPlaceholders = [auth0Domain: "@string/com_auth0_domain", auth0Scheme: "@string/com_auth0_scheme"] } buildFeatures { diff --git a/sample/src/main/java/com/alfresco/sample/AuthConfig.kt b/sample/src/main/java/com/alfresco/sample/AuthConfig.kt index 629e103..6517222 100644 --- a/sample/src/main/java/com/alfresco/sample/AuthConfig.kt +++ b/sample/src/main/java/com/alfresco/sample/AuthConfig.kt @@ -3,6 +3,16 @@ package com.alfresco.sample import com.alfresco.auth.AuthConfig val AuthConfig.Companion.defaultConfig: AuthConfig + get() = AuthConfig( + https = true, + port = "443", + clientId = "zTSfC3VEcrguvP5CKIgNIBCOTliawIKg", + realm = "", + redirectUrl = "demo://dev-ps-alfresco.auth0.com/android/com.alfresco.sample/callback", + contentServicePath = "alfresco", + scheme = "demo" + ) +/*val AuthConfig.Companion.defaultConfig: AuthConfig get() = AuthConfig( https = true, port = "443", @@ -10,4 +20,4 @@ val AuthConfig.Companion.defaultConfig: AuthConfig realm = "alfresco", redirectUrl = "androidacsapp://aims/auth", contentServicePath = "alfresco" - ) + )*/ diff --git a/sample/src/main/java/com/alfresco/sample/LoginActivity.kt b/sample/src/main/java/com/alfresco/sample/LoginActivity.kt index 8d2858e..a61db4f 100644 --- a/sample/src/main/java/com/alfresco/sample/LoginActivity.kt +++ b/sample/src/main/java/com/alfresco/sample/LoginActivity.kt @@ -33,7 +33,7 @@ class LoginViewModel(override var context: Context) : AuthenticationViewModel() } private fun onAuthType(authType: AuthType) { - if (authType == AuthType.PKCE) { + if (authType == AuthType.PKCE || authType == AuthType.OIDC) { pkceLogin(server, AuthConfig.defaultConfig) } else { _onError.value = context.getString(R.string.auth_error_check_connect_url) @@ -76,7 +76,7 @@ class LoginActivity : AuthenticationActivity() { this, credentials.username, credentials.authState, - credentials.authType, + AuthType.OIDC.value, viewModel.applicationUrl ) navigateToMain() diff --git a/sample/src/main/java/com/alfresco/sample/MainViewModel.kt b/sample/src/main/java/com/alfresco/sample/MainViewModel.kt index 9944f91..72e4950 100644 --- a/sample/src/main/java/com/alfresco/sample/MainViewModel.kt +++ b/sample/src/main/java/com/alfresco/sample/MainViewModel.kt @@ -16,7 +16,7 @@ import com.alfresco.content.apis.NodesApi import com.alfresco.content.apis.SearchApi import com.alfresco.content.apis.TrashcanApi import com.alfresco.content.apis.advanceSearch -import com.alfresco.content.models.AppConfigModel +import com.alfreco.common.models.AppConfigModel import com.alfresco.content.models.RequestDefaults import com.alfresco.content.models.RequestFacetField import com.alfresco.content.models.RequestFacetFields @@ -50,7 +50,7 @@ class MainViewModel(private val context: Context) : ViewModel() { private val loggingInterceptor: HttpLoggingInterceptor val results = MutableLiveData>() - val resultsConfig = MutableLiveData() + val resultsConfig = MutableLiveData() val onError = MutableLiveEvent() val onSessionExpired = MutableLiveEvent() diff --git a/settings.gradle b/settings.gradle index 7154fa7..257a8af 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,4 +3,4 @@ include ':sample',':auth', 'content', 'content-ktx', ':process' // Enable Gradle's version catalog support // Ref: https://docs.gradle.org/current/userguide/platforms.html -//enableFeaturePreview("VERSION_CATALOGS") +//enableFeaturePreview("VERSION_CATALOGS") \ No newline at end of file From e3845c12349d789fcf58aaa629f77645dc6bfe02 Mon Sep 17 00:00:00 2001 From: Amanpal Singh <87360222+aman-alfresco@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:34:06 +0530 Subject: [PATCH 2/3] added secured parameter for Auth0 --- .../main/java/com/alfresco/auth/AuthConfig.kt | 11 ++- .../java/com/alfresco/auth/AuthInterceptor.kt | 18 ++++- .../java/com/alfresco/auth/Credentials.kt | 6 +- .../com/alfresco/auth/DiscoveryService.kt | 50 ++++++++++++- .../com/alfresco/auth/pkce/PkceAuthService.kt | 74 +++++++++---------- .../auth/ui/AuthenticationActivity.kt | 13 ++-- .../alfresco/auth/ui/EndSessionActivity.kt | 30 ++++++-- .../com/alfresco/sample/LogoutActivity.kt | 24 +++++- .../java/com/alfresco/sample/MainViewModel.kt | 4 +- 9 files changed, 166 insertions(+), 64 deletions(-) diff --git a/auth/src/main/java/com/alfresco/auth/AuthConfig.kt b/auth/src/main/java/com/alfresco/auth/AuthConfig.kt index 2e338c2..4eec86f 100644 --- a/auth/src/main/java/com/alfresco/auth/AuthConfig.kt +++ b/auth/src/main/java/com/alfresco/auth/AuthConfig.kt @@ -41,9 +41,13 @@ data class AuthConfig( var contentServicePath: String, /** - * Path to content service + * scheme for Auth0 + */ + var scheme: String = "", + /** + * selected AuthType */ - var scheme: String = "" + var authType: String = "" ) { /** * Convenience method for JSON serialization. @@ -56,7 +60,8 @@ data class AuthConfig( /** * Convenience method for deserializing a JSON string representation. */ - @JvmStatic fun jsonDeserialize(str: String): AuthConfig? { + @JvmStatic + fun jsonDeserialize(str: String): AuthConfig? { return try { Json.decodeFromString(serializer(), str) } catch (ex: SerializationException) { diff --git a/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt b/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt index 11b388d..ca3f000 100644 --- a/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt +++ b/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt @@ -227,7 +227,23 @@ class AuthInterceptor( private inner class OIDCProvider (val accessToken : String) : Provider{ override fun intercept(chain: Interceptor.Chain): Response { - return chain.proceed(AuthType.OIDC, accessToken) + val response = chain.proceed(AuthType.OIDC, accessToken) + + // When unauthorized try to refresh +// if (response.code == HTTP_RESPONSE_401_UNAUTHORIZED) { +// val newState = refreshTokenNow() +// +// if (newState != null) { +// response.close() +// response = chain.proceed(AuthType.PKCE, newState.accessToken) +// } +// } + + // If still error notify listener of failure + if (response.code == HTTP_RESPONSE_401_UNAUTHORIZED) { + listener?.onAuthFailure(accountId, response.request.url.toString()) + } + return response } override fun finish() { diff --git a/auth/src/main/java/com/alfresco/auth/Credentials.kt b/auth/src/main/java/com/alfresco/auth/Credentials.kt index 14ee237..1e5255a 100644 --- a/auth/src/main/java/com/alfresco/auth/Credentials.kt +++ b/auth/src/main/java/com/alfresco/auth/Credentials.kt @@ -17,5 +17,9 @@ data class Credentials( /** * String representation of authentication type. */ - val authType: String + val authType: String, + + val hostName: String = "", + + val clientId: String = "" ) diff --git a/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt b/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt index 906e170..c93f4d5 100644 --- a/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt +++ b/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt @@ -2,6 +2,7 @@ package com.alfresco.auth import android.content.Context import android.net.Uri +import com.alfresco.auth.data.AppConfigDetails import com.alfresco.auth.data.ContentServerDetails import com.alfresco.auth.data.ContentServerDetailsData import com.alfresco.auth.pkce.PkceAuthService @@ -24,6 +25,20 @@ class DiscoveryService( * Determine which [AuthType] is supported by the [endpoint]. */ suspend fun getAuthType(endpoint: String): AuthType { + + when (authConfig.authType.lowercase()) { + AuthType.OIDC.value -> { + if (isOIDC(endpoint)){ + return AuthType.OIDC + } + } + AuthType.PKCE.value -> { + if (isPkceType(endpoint)){ + return AuthType.PKCE + } + } + } + return when { isPkceType(endpoint) -> AuthType.PKCE @@ -64,6 +79,31 @@ class DiscoveryService( } } + suspend fun isOIDCInstalled(appConfigURL: String): Boolean { + val uri = PkceAuthService.discoveryUriWithAuth0(appConfigURL).toString() + return withContext(Dispatchers.IO) { + try { + val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .build() + val request = Request.Builder() + .url(URL(uri)) + .get() + .build() + val response = client.newCall(request).execute() + + if (response.code != 200) return@withContext false + + val body = response.body?.string() ?: "" + val data = AppConfigDetails.jsonDeserialize(body) + return@withContext data?.oauth2?.audience?.isNotBlank() == true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + } + /** * returns content server details based on [endpoint]. */ @@ -105,9 +145,7 @@ class DiscoveryService( return result != null } - private fun isOIDC(endpoint: String): Boolean { - return authConfig.realm.isBlank() - } + private suspend fun isOIDC(endpoint: String): Boolean = isOIDCInstalled(endpoint) && authConfig.realm.isBlank() /** * Return content service url based on [endpoint]. @@ -118,6 +156,12 @@ class DiscoveryService( .appendPath(authConfig.contentServicePath) .build() + fun oidcUrl(endpoint: String): Uri = + PkceAuthService.endpointWith(endpoint, authConfig) + .buildUpon() + .appendPath("alfresco") + .build() + private fun contentServiceDiscoveryUrl(endpoint: String): Uri = contentServiceUrl(endpoint) .buildUpon() diff --git a/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt b/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt index 08046fd..eeac098 100644 --- a/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt +++ b/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt @@ -10,9 +10,9 @@ import com.alfresco.auth.R import com.alfresco.auth.data.AppConfigDetails import com.alfresco.auth.data.OAuth2Data import com.alfresco.auth.ui.AuthenticationActivity +import com.alfresco.auth.ui.EndSessionActivity import com.auth0.android.Auth0 import com.auth0.android.authentication.AuthenticationException -import com.auth0.android.callback.Callback import com.auth0.android.jwt.JWT import com.auth0.android.provider.WebAuthProvider import com.auth0.android.result.Credentials @@ -134,7 +134,6 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf println("PkceAuthService.initiateLogin $authDetails") val credentials = webAuthAsync( - authConfig, oauth2, activity ) @@ -142,13 +141,12 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf withContext(Dispatchers.Main) { (activity as AuthenticationActivity<*>).handleResult( credentials, - authConfig + authDetails ) } } } -// discoveryUriWithAuth0() } else { val discoveryUri = discoveryUriWith(endpoint, authConfig) @@ -168,52 +166,52 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf } } - private suspend fun loginWithBrowser( - account: Auth0, - authConfig: AuthConfig, - endpoint: String, - activity: Activity - ) { - // Setup the WebAuthProvider, using the custom scheme and scope. - WebAuthProvider.login(account) - .withScheme(authConfig.scheme) - .withScope("openid profile email read:current_user update:current_user_metadata") - .withAudience("https://${endpoint}/api/v2/") - - // Launch the authentication passing the callback where the results will be received - .start(activity, object : Callback { - override fun onFailure(exception: AuthenticationException) { - Log.d("Test OIDC 3 :: ", "Failure: ${exception.getCode()}") - } - - override fun onSuccess(credentials: Credentials) { - Log.d("Test OIDC 4 :: ", "Success: ${credentials.accessToken}") - } - }) - } - private suspend fun webAuthAsync( - authConfig: AuthConfig, oauth2: OAuth2Data, activity: Activity ): Credentials? { return try { - val account = Auth0(oauth2.clientId, oauth2.host) + val host = URL(oauth2.host).host + Log.d("Test OIDC -1 :: ", oauth2.clientId) + Log.d("Test OIDC 0 :: ", host) + Log.d("Test OIDC 1 :: ", activity.getString(R.string.com_auth0_scheme)) + Log.d("Test OIDC 2 :: ", oauth2.audience) + Log.d("Test OIDC 3 :: ", oauth2.scope) + + val account = Auth0(oauth2.clientId, host) val credentials = WebAuthProvider.login(account) - .withScheme(authConfig.scheme) + .withTrustedWebActivity() + .withScheme(activity.getString(R.string.com_auth0_scheme)) + .withScope(oauth2.scope) .withAudience(oauth2.audience) - .withScope("openid profile email") .await(activity) Log.d("Test OIDC 4 :: ", "Success: ${credentials.accessToken}") credentials } catch (error: AuthenticationException) { val message = if (error.isCanceled) "Browser was closed" else error.getDescription() - Log.d("Test OIDC 3 :: ", "Failure: $message") + Log.d("Test OIDC 5 :: ", "Failure: $message") null } } + + suspend fun logoutAuth0(hostName: String, clientId: String, activity: Activity, requestCode: Int) { + withContext(Dispatchers.IO) { + + val account = Auth0(clientId, hostName) + WebAuthProvider.logout(account) + .withScheme(activity.getString(R.string.com_auth0_scheme)) + .await(activity) + + withContext(Dispatchers.Main) { + (activity as EndSessionActivity<*>).handleResult(requestCode) + } + + + } + } + fun initiateReLogin(activity: Activity, requestCode: Int) { requireNotNull(authState.get()) @@ -370,10 +368,12 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf * @throws [IllegalArgumentException] */ private fun checkConfig(authConfig: AuthConfig) { - require(authConfig.contentServicePath.isNotBlank()) { "Content service path is blank or empty" } -// require(authConfig.realm.isNotBlank()) { "Realm is blank or empty" } - require(authConfig.clientId.isNotBlank()) { "Client id is blank or empty" } - require(authConfig.redirectUrl.isNotBlank()) { "Redirect url is blank or empty" } + if (authConfig.scheme.isBlank()) { + require(authConfig.contentServicePath.isNotBlank()) { "Content service path is blank or empty" } + require(authConfig.realm.isNotBlank()) { "Realm is blank or empty" } + require(authConfig.clientId.isNotBlank()) { "Client id is blank or empty" } + require(authConfig.redirectUrl.isNotBlank()) { "Redirect url is blank or empty" } + } } companion object { diff --git a/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt b/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt index 80e8ab7..4640b6b 100644 --- a/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt +++ b/auth/src/main/java/com/alfresco/auth/ui/AuthenticationActivity.kt @@ -14,6 +14,7 @@ import com.alfresco.auth.Credentials import com.alfresco.auth.DiscoveryService import com.alfresco.auth.data.LiveEvent import com.alfresco.auth.data.MutableLiveEvent +import com.alfresco.auth.data.OAuth2Data import com.alfresco.auth.pkce.PkceAuthService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -45,9 +46,7 @@ abstract class AuthenticationViewModel : ViewModel() { * Check which [AuthType] is supported by the [endpoint] based on the provided [authConfig]. */ fun checkAuthType( - endpoint: String, - authConfig: AuthConfig, - onResult: (authType: AuthType) -> Unit + endpoint: String, authConfig: AuthConfig, onResult: (authType: AuthType) -> Unit ) = viewModelScope.launch { discoveryService = DiscoveryService(context, authConfig) val authType = withContext(Dispatchers.IO) { discoveryService.getAuthType(endpoint) } @@ -129,12 +128,12 @@ abstract class AuthenticationViewModel : ViewModel() { } } - fun handleActivityResult(credentials: com.auth0.android.result.Credentials) { + fun handleActivityResult(credentials: com.auth0.android.result.Credentials, oauth2: OAuth2Data) { viewModelScope.launch { try { val result = credentials.accessToken val userEmail = credentials.user.email ?: "" - _onCredentials.value = Credentials(userEmail, result, AuthType.PKCE.value) + _onCredentials.value = Credentials(userEmail, result, AuthType.OIDC.value, oauth2.host, oauth2.clientId) } catch (ex: Exception) { _onError.value = ex.message ?: "" } @@ -179,8 +178,8 @@ abstract class AuthenticationActivity : AppCompatAc super.onActivityResult(requestCode, resultCode, data) } - fun handleResult(data: com.auth0.android.result.Credentials?, authConfig: AuthConfig) { - data?.let { viewModel.pkceAuth.handleActivityResult(it) } + fun handleResult(data: com.auth0.android.result.Credentials?, oauth2: OAuth2Data) { + data?.let { viewModel.pkceAuth.handleActivityResult(it, oauth2) } } /** diff --git a/auth/src/main/java/com/alfresco/auth/ui/EndSessionActivity.kt b/auth/src/main/java/com/alfresco/auth/ui/EndSessionActivity.kt index 69b6405..8acc843 100644 --- a/auth/src/main/java/com/alfresco/auth/ui/EndSessionActivity.kt +++ b/auth/src/main/java/com/alfresco/auth/ui/EndSessionActivity.kt @@ -12,6 +12,7 @@ import com.alfresco.auth.pkce.PkceAuthService import kotlinx.coroutines.launch import net.openid.appauth.AuthState import org.json.JSONException +import java.net.URL /** * Companion [ViewModel] to [EndSessionActivity] for invoking the logout procedure. @@ -20,7 +21,9 @@ open class EndSessionViewModel( context: Context, authType: AuthType?, authState: String, - authConfig: AuthConfig + authConfig: AuthConfig, + val hostName: String, + val clientId: String ) : ViewModel() { private val authType = authType private val authService: PkceAuthService? @@ -32,7 +35,7 @@ open class EndSessionViewModel( null } - authService = if (authType == AuthType.PKCE) { + authService = if (authType == AuthType.PKCE || authType == AuthType.OIDC) { PkceAuthService(context, state, authConfig) } else { null @@ -44,11 +47,17 @@ open class EndSessionViewModel( */ fun logout(activity: Activity, requestCode: Int) { viewModelScope.launch { - if (authType == AuthType.PKCE) { - authService?.endSession(activity, requestCode) - } else { - activity.setResult(Activity.RESULT_OK) - activity.finish() + when (authType) { + AuthType.PKCE -> { + authService?.endSession(activity, requestCode) + } + AuthType.OIDC -> { + authService?.logoutAuth0(URL(hostName).host,clientId, activity, requestCode) + } + else -> { + activity.setResult(Activity.RESULT_OK) + activity.finish() + } } } } @@ -82,6 +91,13 @@ abstract class EndSessionActivity : AppCompatActivi } } + fun handleResult(requestCode: Int) { + if (requestCode == REQUEST_CODE_END_SESSION) { + setResult(Activity.RESULT_OK) + finish() + } + } + private companion object { const val REQUEST_CODE_END_SESSION = 1 } diff --git a/sample/src/main/java/com/alfresco/sample/LogoutActivity.kt b/sample/src/main/java/com/alfresco/sample/LogoutActivity.kt index 136446b..e0fe5da 100644 --- a/sample/src/main/java/com/alfresco/sample/LogoutActivity.kt +++ b/sample/src/main/java/com/alfresco/sample/LogoutActivity.kt @@ -9,12 +9,26 @@ import com.alfresco.auth.AuthType import com.alfresco.auth.ui.EndSessionActivity import com.alfresco.auth.ui.EndSessionViewModel -class LogoutViewModel(context: Context, authType: AuthType?, authState: String, authConfig: AuthConfig) : EndSessionViewModel(context, authType, authState, authConfig) { +class LogoutViewModel( + context: Context, + authType: AuthType?, + authState: String, + authConfig: AuthConfig, + hostName: String, + clientId: String +) : EndSessionViewModel(context, authType, authState, authConfig, hostName, clientId) { companion object { fun build(context: Context): LogoutViewModel { val acc = requireNotNull(Account.getAccount(context)) - return LogoutViewModel(context, AuthType.PKCE, acc.authState, AuthConfig.defaultConfig) + return LogoutViewModel( + context, + AuthType.PKCE, + acc.authState, + AuthConfig.defaultConfig, + "", + "" + ) } } @@ -32,5 +46,9 @@ class LogoutViewModel(context: Context, authType: AuthType?, authState: String, class LogoutActivity : EndSessionActivity() { - override val viewModel: LogoutViewModel by viewModels { LogoutViewModel.Factory(applicationContext) } + override val viewModel: LogoutViewModel by viewModels { + LogoutViewModel.Factory( + applicationContext + ) + } } diff --git a/sample/src/main/java/com/alfresco/sample/MainViewModel.kt b/sample/src/main/java/com/alfresco/sample/MainViewModel.kt index 72e4950..9944f91 100644 --- a/sample/src/main/java/com/alfresco/sample/MainViewModel.kt +++ b/sample/src/main/java/com/alfresco/sample/MainViewModel.kt @@ -16,7 +16,7 @@ import com.alfresco.content.apis.NodesApi import com.alfresco.content.apis.SearchApi import com.alfresco.content.apis.TrashcanApi import com.alfresco.content.apis.advanceSearch -import com.alfreco.common.models.AppConfigModel +import com.alfresco.content.models.AppConfigModel import com.alfresco.content.models.RequestDefaults import com.alfresco.content.models.RequestFacetField import com.alfresco.content.models.RequestFacetFields @@ -50,7 +50,7 @@ class MainViewModel(private val context: Context) : ViewModel() { private val loggingInterceptor: HttpLoggingInterceptor val results = MutableLiveData>() - val resultsConfig = MutableLiveData() + val resultsConfig = MutableLiveData() val onError = MutableLiveEvent() val onSessionExpired = MutableLiveEvent() From 228e1bc941c06dac97494266002d77bd3ebb7b30 Mon Sep 17 00:00:00 2001 From: Amanpal Singh <87360222+aman-alfresco@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:04:43 +0530 Subject: [PATCH 3/3] code correction --- .../main/java/com/alfresco/auth/AuthInterceptor.kt | 10 ---------- .../main/java/com/alfresco/auth/DiscoveryService.kt | 8 +++++--- .../java/com/alfresco/auth/pkce/PkceAuthService.kt | 11 ++--------- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt b/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt index ca3f000..f46e150 100644 --- a/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt +++ b/auth/src/main/java/com/alfresco/auth/AuthInterceptor.kt @@ -229,16 +229,6 @@ class AuthInterceptor( override fun intercept(chain: Interceptor.Chain): Response { val response = chain.proceed(AuthType.OIDC, accessToken) - // When unauthorized try to refresh -// if (response.code == HTTP_RESPONSE_401_UNAUTHORIZED) { -// val newState = refreshTokenNow() -// -// if (newState != null) { -// response.close() -// response = chain.proceed(AuthType.PKCE, newState.accessToken) -// } -// } - // If still error notify listener of failure if (response.code == HTTP_RESPONSE_401_UNAUTHORIZED) { listener?.onAuthFailure(accountId, response.request.url.toString()) diff --git a/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt b/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt index c93f4d5..708985d 100644 --- a/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt +++ b/auth/src/main/java/com/alfresco/auth/DiscoveryService.kt @@ -28,12 +28,13 @@ class DiscoveryService( when (authConfig.authType.lowercase()) { AuthType.OIDC.value -> { - if (isOIDC(endpoint)){ + if (isOIDC(endpoint)) { return AuthType.OIDC } } + AuthType.PKCE.value -> { - if (isPkceType(endpoint)){ + if (isPkceType(endpoint)) { return AuthType.PKCE } } @@ -145,7 +146,8 @@ class DiscoveryService( return result != null } - private suspend fun isOIDC(endpoint: String): Boolean = isOIDCInstalled(endpoint) && authConfig.realm.isBlank() + private suspend fun isOIDC(endpoint: String): Boolean = + isOIDCInstalled(endpoint) && authConfig.realm.isBlank() /** * Return content service url based on [endpoint]. diff --git a/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt b/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt index eeac098..f1fadd0 100644 --- a/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt +++ b/auth/src/main/java/com/alfresco/auth/pkce/PkceAuthService.kt @@ -131,7 +131,6 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf val authDetails = getAppConfigOAuth2Details(uri) authDetails?.let { oauth2 -> - println("PkceAuthService.initiateLogin $authDetails") val credentials = webAuthAsync( oauth2, @@ -172,11 +171,7 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf ): Credentials? { return try { val host = URL(oauth2.host).host - Log.d("Test OIDC -1 :: ", oauth2.clientId) - Log.d("Test OIDC 0 :: ", host) - Log.d("Test OIDC 1 :: ", activity.getString(R.string.com_auth0_scheme)) - Log.d("Test OIDC 2 :: ", oauth2.audience) - Log.d("Test OIDC 3 :: ", oauth2.scope) + val account = Auth0(oauth2.clientId, host) val credentials = WebAuthProvider.login(account) @@ -185,12 +180,10 @@ internal class PkceAuthService(context: Context, authState: AuthState?, authConf .withScope(oauth2.scope) .withAudience(oauth2.audience) .await(activity) - Log.d("Test OIDC 4 :: ", "Success: ${credentials.accessToken}") - credentials + credentials } catch (error: AuthenticationException) { val message = if (error.isCanceled) "Browser was closed" else error.getDescription() - Log.d("Test OIDC 5 :: ", "Failure: $message") null } }