diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5a56e49193..3c4380170a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,21 +23,22 @@ android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true"> + + - + - @@ -50,25 +51,31 @@ android:scheme="https" /> + + + + + + + + @@ -118,6 +127,7 @@ + { + delay(3, TimeUnit.SECONDS) + client.loginWithSaml(credentialToken) + } TYPE_LOGIN_OAUTH -> { client.loginWithOauth(credentialToken, credentialSecret) } @@ -319,7 +346,7 @@ class LoginPresenter @Inject constructor( } } else -> { - throw IllegalStateException("Expected TYPE_LOGIN_USER_EMAIL, TYPE_LOGIN_CAS, TYPE_LOGIN_OAUTH or TYPE_LOGIN_DEEP_LINK") + throw IllegalStateException("Expected TYPE_LOGIN_USER_EMAIL, TYPE_LOGIN_CAS,TYPE_LOGIN_SAML, TYPE_LOGIN_OAUTH or TYPE_LOGIN_DEEP_LINK") } } } @@ -365,6 +392,18 @@ class LoginPresenter @Inject constructor( }.toString() } + private fun getSamlServices(listMap: List>): List> { + return listMap.filter { map -> map["service"] == "saml" } + } + + private fun getSamlServiceName(service: Map): String { + return service["buttonLabelText"].toString() + } + + private fun getSamlProvider(service: Map): String { + return (service["clientConfig"] as Map<*, *>)["provider"].toString() + } + private fun getCustomOauthServices(listMap: List>): List> { return listMap.filter { map -> map["custom"] == true } } @@ -389,11 +428,11 @@ class LoginPresenter @Inject constructor( return service["scope"].toString() } - private fun getCustomOauthButtonColor(service: Map): Int { + private fun getServiceButtonColor(service: Map): Int { return service["buttonColor"].toString().parseColor() } - private fun getCustomOauthServiceNameColor(service: Map): Int { + private fun getServiceNameColor(service: Map): Int { return service["buttonLabelColor"].toString().parseColor() } diff --git a/app/src/main/java/chat/rocket/android/authentication/login/presentation/LoginView.kt b/app/src/main/java/chat/rocket/android/authentication/login/presentation/LoginView.kt index ff33b7e18c..bc1ec400ce 100644 --- a/app/src/main/java/chat/rocket/android/authentication/login/presentation/LoginView.kt +++ b/app/src/main/java/chat/rocket/android/authentication/login/presentation/LoginView.kt @@ -87,7 +87,7 @@ interface LoginView : LoadingView, MessageView { * Enables and shows the oauth view if there is login via social accounts enabled by the server settings. * * REMARK: We must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle], - * [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter], [enableLoginByGitlab] or [addCustomOauthServiceButton]) for the oauth view. + * [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter], [enableLoginByGitlab], [addCustomOauthServiceButton] or [addSamlServiceButton]) for the oauth view. * If the possibility of login via social accounts exceeds 3 different ways we should set up the FAB ([setupFabListener]) to show the remaining view(s). */ fun enableOauthView() @@ -197,7 +197,7 @@ interface LoginView : LoadingView, MessageView { * @state A random string generated by the app, which you'll verify later (to protect against forgery attacks). * @serviceName The custom OAuth service name. * @serviceNameColor The custom OAuth service name color (just stylizing). - * @buttonColor The color of the custom OAuth button (just stylizing). + * @buttonColor The custom OAuth button color (just stylizing). * @see [enableOauthView] */ fun addCustomOauthServiceButton( @@ -208,6 +208,23 @@ interface LoginView : LoadingView, MessageView { buttonColor: Int ) + /** + * Adds a SAML button in the oauth view. + * + * @samlUrl The SAML url to sets up the button (the listener). + * @serviceName The SAML service name. + * @serviceNameColor The SAML service name color (just stylizing). + * @buttonColor The SAML button color (just stylizing). + * @see [enableOauthView] + */ + fun addSamlServiceButton( + samlUrl: String, + samlToken: String, + serviceName: String, + serviceNameColor: Int, + buttonColor: Int + ) + /** * Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)). */ diff --git a/app/src/main/java/chat/rocket/android/authentication/login/ui/LoginFragment.kt b/app/src/main/java/chat/rocket/android/authentication/login/ui/LoginFragment.kt index 558acec9c4..2edf0f8292 100644 --- a/app/src/main/java/chat/rocket/android/authentication/login/ui/LoginFragment.kt +++ b/app/src/main/java/chat/rocket/android/authentication/login/ui/LoginFragment.kt @@ -25,8 +25,8 @@ import chat.rocket.android.authentication.login.presentation.LoginView import chat.rocket.android.helper.KeyboardHelper import chat.rocket.android.helper.TextHelper import chat.rocket.android.util.extensions.* -import chat.rocket.android.webview.cas.ui.INTENT_CAS_TOKEN -import chat.rocket.android.webview.cas.ui.casWebViewIntent +import chat.rocket.android.webview.sso.ui.INTENT_SSO_TOKEN +import chat.rocket.android.webview.sso.ui.ssoWebViewIntent import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_SECRET import chat.rocket.android.webview.oauth.ui.INTENT_OAUTH_CREDENTIAL_TOKEN import chat.rocket.android.webview.oauth.ui.oauthWebViewIntent @@ -42,12 +42,12 @@ import kotlinx.android.synthetic.main.fragment_authentication_log_in.* import timber.log.Timber import javax.inject.Inject - internal const val REQUEST_CODE_FOR_CAS = 1 -internal const val REQUEST_CODE_FOR_OAUTH = 2 -internal const val MULTIPLE_CREDENTIALS_READ = 3 -internal const val NO_CREDENTIALS_EXIST = 4 -internal const val SAVE_CREDENTIALS = 5 +internal const val REQUEST_CODE_FOR_SAML = 2 +internal const val REQUEST_CODE_FOR_OAUTH = 3 +internal const val MULTIPLE_CREDENTIALS_READ = 4 +internal const val NO_CREDENTIALS_EXIST = 5 +internal const val SAVE_CREDENTIALS = 6 lateinit var googleApiClient: GoogleApiClient @@ -121,7 +121,10 @@ class LoginFragment : Fragment(), LoginView, GoogleApiClient.ConnectionCallbacks if (data != null) { when (requestCode) { REQUEST_CODE_FOR_CAS -> data.apply { - presenter.authenticateWithCas(getStringExtra(INTENT_CAS_TOKEN)) + presenter.authenticateWithCas(getStringExtra(INTENT_SSO_TOKEN)) + } + REQUEST_CODE_FOR_SAML -> data.apply { + presenter.authenticateWithSaml(getStringExtra(INTENT_SSO_TOKEN)) } REQUEST_CODE_FOR_OAUTH -> { isOauthSuccessful = true @@ -371,7 +374,7 @@ class LoginFragment : Fragment(), LoginView, GoogleApiClient.ConnectionCallbacks ui { activity -> button_cas.setOnClickListener { startActivityForResult( - activity.casWebViewIntent(casUrl, casToken), + activity.ssoWebViewIntent(casUrl, casToken), REQUEST_CODE_FOR_CAS ) activity.overridePendingTransition(R.anim.slide_up, R.anim.hold) @@ -566,7 +569,7 @@ class LoginFragment : Fragment(), LoginView, GoogleApiClient.ConnectionCallbacks buttonColor: Int ) { ui { activity -> - val button = getCustomOauthButton(serviceName, serviceNameColor, buttonColor) + val button = getCustomServiceButton(serviceName, serviceNameColor, buttonColor) social_accounts_container.addView(button) button.setOnClickListener { @@ -579,6 +582,27 @@ class LoginFragment : Fragment(), LoginView, GoogleApiClient.ConnectionCallbacks } } + override fun addSamlServiceButton( + samlUrl: String, + samlToken: String, + serviceName: String, + serviceNameColor: Int, + buttonColor: Int + ) { + ui { activity -> + val button = getCustomServiceButton(serviceName, serviceNameColor, buttonColor) + social_accounts_container.addView(button) + + button.setOnClickListener { + startActivityForResult( + activity.ssoWebViewIntent(samlUrl, samlToken), + REQUEST_CODE_FOR_SAML + ) + activity.overridePendingTransition(R.anim.slide_up, R.anim.hold) + } + } + } + override fun setupFabListener() { ui { button_fab.isVisible = true @@ -696,9 +720,9 @@ class LoginFragment : Fragment(), LoginView, GoogleApiClient.ConnectionCallbacks } /** - * Gets a stylized custom OAuth button. + * Gets a stylized custom service button. */ - private fun getCustomOauthButton( + private fun getCustomServiceButton( buttonText: String, buttonTextColor: Int, buttonBgColor: Int diff --git a/app/src/main/java/chat/rocket/android/util/extensions/String.kt b/app/src/main/java/chat/rocket/android/util/extensions/String.kt index 5750846b7c..2d9f88e3e2 100644 --- a/app/src/main/java/chat/rocket/android/util/extensions/String.kt +++ b/app/src/main/java/chat/rocket/android/util/extensions/String.kt @@ -40,8 +40,11 @@ fun String.safeUrl(): String { fun String.serverLogoUrl(favicon: String) = "${removeTrailingSlash()}/$favicon" -fun String.casUrl(serverUrl: String, token: String) = - "${removeTrailingSlash()}?service=${serverUrl.removeTrailingSlash()}/_cas/$token" +fun String.casUrl(serverUrl: String, casToken: String) = + "${removeTrailingSlash()}?service=${serverUrl.removeTrailingSlash()}/_cas/$casToken" + +fun String.samlUrl(provider: String, samlToken: String) = + "${removeTrailingSlash()}/_saml/authorize/$provider/$samlToken" fun String.termsOfServiceUrl() = "${removeTrailingSlash()}/terms-of-service" diff --git a/app/src/main/java/chat/rocket/android/webview/oauth/ui/OauthWebViewActivity.kt b/app/src/main/java/chat/rocket/android/webview/oauth/ui/OauthWebViewActivity.kt index 0a7a66a321..3a8c091a5a 100644 --- a/app/src/main/java/chat/rocket/android/webview/oauth/ui/OauthWebViewActivity.kt +++ b/app/src/main/java/chat/rocket/android/webview/oauth/ui/OauthWebViewActivity.kt @@ -36,7 +36,6 @@ class OauthWebViewActivity : AppCompatActivity() { private lateinit var state: String private var isWebViewSetUp: Boolean = false - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_web_view) diff --git a/app/src/main/java/chat/rocket/android/webview/cas/ui/CasWebViewActivity.kt b/app/src/main/java/chat/rocket/android/webview/sso/ui/SsoWebViewActivity.kt similarity index 69% rename from app/src/main/java/chat/rocket/android/webview/cas/ui/CasWebViewActivity.kt rename to app/src/main/java/chat/rocket/android/webview/sso/ui/SsoWebViewActivity.kt index 687be81271..2df31be4d3 100644 --- a/app/src/main/java/chat/rocket/android/webview/cas/ui/CasWebViewActivity.kt +++ b/app/src/main/java/chat/rocket/android/webview/sso/ui/SsoWebViewActivity.kt @@ -1,4 +1,4 @@ -package chat.rocket.android.webview.cas.ui +package chat.rocket.android.webview.sso.ui import android.annotation.SuppressLint import android.app.Activity @@ -13,19 +13,23 @@ import chat.rocket.android.R import kotlinx.android.synthetic.main.activity_web_view.* import kotlinx.android.synthetic.main.app_bar.* -fun Context.casWebViewIntent(webPageUrl: String, casToken: String): Intent { - return Intent(this, CasWebViewActivity::class.java).apply { +fun Context.ssoWebViewIntent(webPageUrl: String, casToken: String): Intent { + return Intent(this, SsoWebViewActivity::class.java).apply { putExtra(INTENT_WEB_PAGE_URL, webPageUrl) - putExtra(INTENT_CAS_TOKEN, casToken) + putExtra(INTENT_SSO_TOKEN, casToken) } } private const val INTENT_WEB_PAGE_URL = "web_page_url" -const val INTENT_CAS_TOKEN = "cas_token" +const val INTENT_SSO_TOKEN = "cas_token" -class CasWebViewActivity : AppCompatActivity() { +/** + * This class is responsible to handle the authentication thought single sign-on protocol (CAS and SAML). + */ +class SsoWebViewActivity : AppCompatActivity() { private lateinit var webPageUrl: String private lateinit var casToken: String + private var isWebViewSetUp: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -34,7 +38,7 @@ class CasWebViewActivity : AppCompatActivity() { webPageUrl = intent.getStringExtra(INTENT_WEB_PAGE_URL) requireNotNull(webPageUrl) { "no web_page_url provided in Intent extras" } - casToken = intent.getStringExtra(INTENT_CAS_TOKEN) + casToken = intent.getStringExtra(INTENT_SSO_TOKEN) requireNotNull(casToken) { "no cas_token provided in Intent extras" } setupToolbar() @@ -42,7 +46,10 @@ class CasWebViewActivity : AppCompatActivity() { override fun onResume() { super.onResume() - setupWebView() + if (!isWebViewSetUp) { + setupWebView() + isWebViewSetUp = true + } } override fun onBackPressed() { @@ -64,15 +71,16 @@ class CasWebViewActivity : AppCompatActivity() { web_view.settings.javaScriptEnabled = true web_view.webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { - // The user may have already been logged in the CAS, so check if the URL contains the "ticket" word - // (that means the user is successful authenticated and we don't need to wait until the page is fully loaded). - if (url.contains("ticket")) { + // The user may have already been logged in the SSO, so check if the URL contains + // the "ticket" or "validate" word (that means the user is successful authenticated + // and we don't need to wait until the page is fully loaded). + if (url.contains("ticket") || url.contains("validate")) { closeView(Activity.RESULT_OK) } } override fun onPageFinished(view: WebView, url: String) { - if (url.contains("ticket")) { + if (url.contains("ticket") || url.contains("validate")) { closeView(Activity.RESULT_OK) } else { view_loading.hide() @@ -83,7 +91,7 @@ class CasWebViewActivity : AppCompatActivity() { } private fun closeView(activityResult: Int = Activity.RESULT_CANCELED) { - setResult(activityResult, Intent().putExtra(INTENT_CAS_TOKEN, casToken)) + setResult(activityResult, Intent().putExtra(INTENT_SSO_TOKEN, casToken)) finish() overridePendingTransition(R.anim.hold, R.anim.slide_down) }