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