Commit dbcbe54c authored by Filipe de Lima Brito's avatar Filipe de Lima Brito

Add CAS login support.

parent c1871a62
......@@ -4,9 +4,11 @@ import chat.rocket.android.authentication.domain.model.TokenModel
import chat.rocket.android.authentication.presentation.AuthenticationNavigator
import chat.rocket.android.core.lifecycle.CancelStrategy
import chat.rocket.android.helper.NetworkHelper
import chat.rocket.android.helper.UrlHelper
import chat.rocket.android.infrastructure.LocalRepository
import chat.rocket.android.server.domain.*
import chat.rocket.android.server.infraestructure.RocketChatClientFactory
import chat.rocket.android.util.extensions.generateRandomString
import chat.rocket.android.util.extensions.isEmailValid
import chat.rocket.android.util.extensions.launchUI
import chat.rocket.common.RocketChatException
......@@ -41,38 +43,56 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
return
}
view.showSignUpView(settings.registrationEnabled())
if (settings.casEnabled()) {
// Hiding the views in order to show the WebView instead.
view.hideUsernameOrEmailView()
view.hidePasswordView()
view.hideSignUpView()
view.hideOauthView()
view.hideLoginButton()
var hasSocial = false
if (settings.facebookEnabled()) {
view.enableLoginByFacebook()
hasSocial = true
}
if (settings.githubEnabled()) {
view.enableLoginByGithub()
hasSocial = true
}
if (settings.googleEnabled()) {
view.enableLoginByGoogle()
hasSocial = true
}
if (settings.linkedinEnabled()) {
view.enableLoginByLinkedin()
hasSocial = true
}
if (settings.meteorEnabled()) {
view.enableLoginByMeteor()
hasSocial = true
}
if (settings.twitterEnabled()) {
view.enableLoginByTwitter()
hasSocial = true
}
if (settings.gitlabEnabled()) {
view.enableLoginByGitlab()
hasSocial = true
view.showLoading()
val token = generateRandomString(17)
view.showCasView(UrlHelper.getCasUrl(settings.casLoginUrl(), server, token), token)
} else {
if (settings.registrationEnabled()){
view.showSignUpView()
}
var hasSocial = false
if (settings.facebookEnabled()) {
view.enableLoginByFacebook()
hasSocial = true
}
if (settings.githubEnabled()) {
view.enableLoginByGithub()
hasSocial = true
}
if (settings.googleEnabled()) {
view.enableLoginByGoogle()
hasSocial = true
}
if (settings.linkedinEnabled()) {
view.enableLoginByLinkedin()
hasSocial = true
}
if (settings.meteorEnabled()) {
view.enableLoginByMeteor()
hasSocial = true
}
if (settings.twitterEnabled()) {
view.enableLoginByTwitter()
hasSocial = true
}
if (settings.gitlabEnabled()) {
view.enableLoginByGitlab()
hasSocial = true
}
if (hasSocial) {
view.showOauthView()
}
}
view.showOauthView(hasSocial)
}
fun authenticate(usernameOrEmail: String, password: String) {
......@@ -110,9 +130,7 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
}
if (token != null) {
val me = client.me()
multiServerRepository.save(server, TokenModel(token.userId, token.authToken))
localRepository.save(LocalRepository.USERNAME_KEY, me.username)
saveToken(server, TokenModel(token.userId, token.authToken), client.me().username)
registerPushToken()
navigator.toChatList()
} else {
......@@ -142,6 +160,41 @@ class LoginPresenter @Inject constructor(private val view: LoginView,
}
}
fun authenticateWithCas(casCredential: String) {
launchUI(strategy) {
if (NetworkHelper.hasInternetAccess()) {
view.showLoading()
try {
val server = serverInteractor.get()
if (server != null) {
val token = client.loginWithCas(casCredential)
saveToken(server, TokenModel(token.userId, token.authToken), client.me().username)
registerPushToken()
navigator.toChatList()
} else {
navigator.toServerScreen()
}
} catch (exception: RocketChatException) {
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
} finally {
view.hideLoading()
}
} else {
view.showNoInternetConnection()
}
}
}
private suspend fun saveToken(server: String, tokenModel: TokenModel, username: String?) {
multiServerRepository.save(server, tokenModel)
localRepository.save(LocalRepository.USERNAME_KEY, username)
registerPushToken()
}
fun signup() = navigator.toSignUp()
private suspend fun registerPushToken() {
......
......@@ -6,64 +6,97 @@ import chat.rocket.android.core.behaviours.MessageView
interface LoginView : LoadingView, MessageView, InternetView {
/**
* Shows the CAS view if the server settings allow the sign in/sign out via CAS protocol.
* @param casUrl The CAS URL to login/sign up with.
* @param requestedToken The requested Token sent to the CAS server.
*/
fun showCasView(casUrl: String, requestedToken: String)
/**
* Shows the sign up view if the server settings allow the new users registration.
*/
fun showSignUpView()
/**
* Shows the oauth view if the server settings allow the login via social accounts.
*
* REMARK: we must show at maximum *three* social accounts views ([enableLoginByFacebook], [enableLoginByGithub], [enableLoginByGoogle],
* [enableLoginByLinkedin], [enableLoginByMeteor], [enableLoginByTwitter] or [enableLoginByGitlab]) 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).
*
* @param value True to show the oauth view, false otherwise.
*/
fun showOauthView(value: Boolean)
fun showOauthView()
/**
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
* Shows the login button.
*/
fun setupFabListener()
fun showLoginButton()
/**
* Shows the login by Facebook view.
* Shows the login by Facebook view if the server settings allow it.
*/
fun enableLoginByFacebook()
/**
* Shows the login by Github view.
* Shows the login by Github view if the server settings allow it.
*/
fun enableLoginByGithub()
/**
* Shows the login by Google view.
* Shows the login by Google view if the server settings allow it.
*/
fun enableLoginByGoogle()
/**
* Shows the login by Linkedin view.
* Shows the login by Linkedin view if the server settings allow it.
*/
fun enableLoginByLinkedin()
/**
* Shows the login by Meteor view.
* Shows the login by Meteor view if the server settings allow it.
*/
fun enableLoginByMeteor()
/**
* Shows the login by Twitter view.
* Shows the login by Twitter view if the server settings allow it.
*/
fun enableLoginByTwitter()
/**
* Shows the login by Gitlab view.
* Shows the login by Gitlab view if the server settings allow it.
*/
fun enableLoginByGitlab()
/**
* Shows the sign up view if the server settings allow the new users registration.
*
* @param value True to show the sign up view, false otherwise.
* Setups the FloatingActionButton to show more social accounts views (expanding the oauth view interface to show the remaining view(s)).
*/
fun showSignUpView(value: Boolean)
fun setupFabListener()
/**
* Hides the username/e-mail view.
*/
fun hideUsernameOrEmailView()
/**
* Hides the password view.
*/
fun hidePasswordView()
/**
* Hides the sign up view if the server settings does not allow the new users registration.
*/
fun hideSignUpView()
/**
* Hides the oauth view.
*/
fun hideOauthView()
/**
* Hides the login button.
*/
fun hideLoginButton()
/**
* Alerts the user about a wrong inputted username or email.
*/
......
package chat.rocket.android.authentication.login.ui
import DrawableHelper
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.support.v4.app.Fragment
......@@ -10,9 +12,10 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ImageButton
import android.widget.ScrollView
import chat.rocket.android.R
import chat.rocket.android.authentication.login.presentation.LoginPresenter
import chat.rocket.android.authentication.login.presentation.LoginView
......@@ -23,7 +26,6 @@ import chat.rocket.android.util.extensions.inflate
import chat.rocket.android.util.extensions.setVisible
import chat.rocket.android.util.extensions.showToast
import chat.rocket.android.util.extensions.textContent
import dagger.android.support.AndroidSupportInjection
import kotlinx.android.synthetic.main.fragment_authentication_log_in.*
import javax.inject.Inject
......@@ -47,7 +49,8 @@ class LoginFragment : Fragment(), LoginView {
AndroidSupportInjection.inject(this)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = container?.inflate(R.layout.fragment_authentication_log_in)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
container?.inflate(R.layout.fragment_authentication_log_in)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
......@@ -67,12 +70,6 @@ class LoginFragment : Fragment(), LoginView {
setupSignUpListener()
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
areLoginOptionsNeeded()
}
override fun onDestroyView() {
super.onDestroyView()
if (isGlobalLayoutListenerSetUp) {
......@@ -81,31 +78,30 @@ class LoginFragment : Fragment(), LoginView {
}
}
override fun showOauthView(value: Boolean) {
if (value) {
social_accounts_container.setVisible(true)
button_fab.setVisible(true)
// We need to setup the layout to hide and show the oauth interface when the soft keyboard is shown
// (means that the user touched the text_username_or_email or text_password EditText to fill that respective fields).
if (!isGlobalLayoutListenerSetUp) {
scroll_view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
isGlobalLayoutListenerSetUp = true
}
} else {
social_accounts_container.setVisible(false)
button_fab.setVisible(false)
override fun showCasView(casUrl: String, requestedToken: String) {
activity?.apply {
// We need to update the view headline, since the CAS allows the log in or sign up
text_headline.textContent = getString(R.string.title_log_in_or_sign_up)
setupWebView(casUrl, requestedToken)
}
}
override fun setupFabListener() {
button_fab.setOnClickListener({
button_fab.hide()
showRemainingSocialAccountsView()
scrollToBottom()
})
override fun showSignUpView() = text_new_to_rocket_chat.setVisible(true)
override fun showOauthView() {
social_accounts_container.setVisible(true)
button_fab.setVisible(true)
// We need to setup the layout to hide and show the oauth interface when the soft keyboard is shown
// (means that the user touched the text_username_or_email or text_password EditText to fill that respective fields).
if (!isGlobalLayoutListenerSetUp) {
scroll_view.viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
isGlobalLayoutListenerSetUp = true
}
}
override fun showLoginButton() = button_log_in.setVisible(true)
override fun enableLoginByFacebook() {
button_facebook.isEnabled = true
}
......@@ -134,7 +130,26 @@ class LoginFragment : Fragment(), LoginView {
button_gitlab.isEnabled = true
}
override fun showSignUpView(value: Boolean) = text_new_to_rocket_chat.setVisible(value)
override fun setupFabListener() {
button_fab.setOnClickListener({
button_fab.hide()
showRemainingSocialAccountsView()
scrollToBottom()
})
}
override fun hideUsernameOrEmailView() = text_username_or_email.setVisible(false)
override fun hidePasswordView() = text_password.setVisible(false)
override fun hideSignUpView() = text_new_to_rocket_chat.setVisible(false)
override fun hideOauthView() {
social_accounts_container.setVisible(false)
button_fab.setVisible(false)
}
override fun hideLoginButton() = button_log_in.setVisible(false)
override fun alertWrongUsernameOrEmail() {
AnimationHelper.vibrateSmartPhone(appContext)
......@@ -164,24 +179,25 @@ class LoginFragment : Fragment(), LoginView {
override fun showGenericErrorMessage() = showMessage(getString(R.string.msg_generic_error))
override fun showNoInternetConnection() = showMessage(getString(R.string.msg_no_internet_connection))
override fun showNoInternetConnection() =
showMessage(getString(R.string.msg_no_internet_connection))
private fun areLoginOptionsNeeded() {
if (!isEditTextEmpty() || KeyboardHelper.isSoftKeyboardShown(scroll_view.rootView)) {
showSignUpView(false)
showOauthView(false)
showLoginButton(true)
hideSignUpView()
hideOauthView()
showLoginButton()
} else {
showSignUpView(true)
showOauthView(true)
showLoginButton(false)
showSignUpView()
showOauthView()
hideLoginButton()
}
}
private fun tintEditTextDrawableStart() {
activity?.apply {
val personDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_assignment_ind_black_24dp, this)
val personDrawable =
DrawableHelper.getDrawableFromId(R.drawable.ic_assignment_ind_black_24dp, this)
val lockDrawable = DrawableHelper.getDrawableFromId(R.drawable.ic_lock_black_24dp, this)
val drawables = arrayOf(personDrawable, lockDrawable)
......@@ -191,10 +207,6 @@ class LoginFragment : Fragment(), LoginView {
}
}
private fun showLoginButton(value: Boolean) {
button_log_in.setVisible(value)
}
private fun setupSignUpListener() {
val signUp = getString(R.string.title_sign_up)
val newToRocketChat = String.format(getString(R.string.msg_new_user), signUp)
......@@ -215,7 +227,8 @@ class LoginFragment : Fragment(), LoginView {
}
// Returns true if *all* EditTexts are empty.
private fun isEditTextEmpty(): Boolean = text_username_or_email.textContent.isBlank() && text_password.textContent.isEmpty()
private fun isEditTextEmpty(): Boolean =
text_username_or_email.textContent.isBlank() && text_password.textContent.isEmpty()
private fun showRemainingSocialAccountsView() {
social_accounts_container.postDelayed({
......@@ -242,4 +255,33 @@ class LoginFragment : Fragment(), LoginView {
scroll_view.fullScroll(ScrollView.FOCUS_DOWN)
}, 1250)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView(webPageUrl: String, requestedToken: String) {
web_view.settings.javaScriptEnabled = true
web_view.webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
// The user can be already logged in the CAS, so check if the URL contains "ticket"
// (that means he/she is successful authenticated and we don't need to wait until the page is finished.
if (url.contains("ticket")) {
authenticateWithCas(requestedToken)
}
}
override fun onPageFinished(view: WebView, url: String) {
if (url.contains("ticket")) {
authenticateWithCas(requestedToken)
} else {
hideLoading()
web_view.setVisible(true)
}
}
}
web_view.loadUrl(webPageUrl)
}
private fun authenticateWithCas(requestedToken: String) {
web_view.setVisible(false)
presenter.authenticateWithCas(requestedToken)
}
}
\ No newline at end of file
......@@ -13,6 +13,17 @@ object UrlHelper {
*/
fun getAvatarUrl(serverUrl: String, avatarName: String): String = removeTrailingSlash(serverUrl) + "/avatar/" + avatarName
/**
* Returns the CAS URL.
*
* @param casLoginUrl The CAS login URL from the server settings.
* @param serverUrl The server URL.
* @param token The token to be send to the CAS server.
* @return The avatar URL.
*/
fun getCasUrl(casLoginUrl: String, serverUrl: String, token: String): String =
removeTrailingSlash(casLoginUrl) + "?service=" + removeTrailingSlash(serverUrl) + "/_cas/" + token
/**
* Returns the server's Terms of Service URL.
*
......
......@@ -14,10 +14,10 @@ class RefreshSettingsInteractor @Inject constructor(private val factory: RocketC
SITE_URL, SITE_NAME, FAVICON_512, USE_REALNAME, ALLOW_ROOM_NAME_SPECIAL_CHARS,
FAVORITE_ROOMS, ACCOUNT_LOGIN_FORM, ACCOUNT_GOOGLE, ACCOUNT_FACEBOOK, ACCOUNT_GITHUB,
ACCOUNT_GITLAB, ACCOUNT_LINKEDIN, ACCOUNT_METEOR, ACCOUNT_TWITTER, ACCOUNT_WORDPRESS,
LDAP_ENABLE, ACCOUNT_REGISTRATION, UPLOAD_STORAGE_TYPE, UPLOAD_MAX_FILE_SIZE,
UPLOAD_WHITELIST_MIMETYPES, HIDE_USER_JOIN, HIDE_USER_LEAVE, HIDE_TYPE_AU, HIDE_MUTE_UNMUTE,
HIDE_TYPE_RU, ACCOUNT_CUSTOM_FIELDS, ALLOW_MESSAGE_DELETING, ALLOW_MESSAGE_EDITING,
ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
LDAP_ENABLE, CAS_ENABLE, CAS_LOGIN_URL, ACCOUNT_REGISTRATION, UPLOAD_STORAGE_TYPE,
UPLOAD_MAX_FILE_SIZE, UPLOAD_WHITELIST_MIMETYPES, HIDE_USER_JOIN, HIDE_USER_LEAVE,
HIDE_TYPE_AU, HIDE_MUTE_UNMUTE, HIDE_TYPE_RU, ACCOUNT_CUSTOM_FIELDS, ALLOW_MESSAGE_DELETING,
ALLOW_MESSAGE_EDITING, ALLOW_MESSAGE_PINNING, SHOW_DELETED_STATUS, SHOW_EDITED_STATUS)
suspend fun refresh(server: String) {
withContext(CommonPool) {
......
......@@ -29,6 +29,8 @@ const val USE_REALNAME = "UI_Use_Real_Name"
const val ALLOW_ROOM_NAME_SPECIAL_CHARS = "UI_Allow_room_names_with_special_chars"
const val FAVORITE_ROOMS = "Favorite_Rooms"
const val LDAP_ENABLE = "LDAP_Enable"
const val CAS_ENABLE = "CAS_enabled"
const val CAS_LOGIN_URL = "CAS_login_url"
const val UPLOAD_STORAGE_TYPE = "FileUpload_Storage_Type"
const val UPLOAD_MAX_FILE_SIZE = "FileUpload_MaxFileSize"
const val UPLOAD_WHITELIST_MIMETYPES = "FileUpload_MediaTypeWhiteList"
......@@ -59,6 +61,8 @@ fun PublicSettings.wordpressEnabled(): Boolean = this[ACCOUNT_WORDPRESS]?.value
fun PublicSettings.useRealName(): Boolean = this[USE_REALNAME]?.value == true
fun PublicSettings.ldapEnabled(): Boolean = this[LDAP_ENABLE]?.value == true
fun PublicSettings.casEnabled(): Boolean = this[CAS_ENABLE]?.value == true
fun PublicSettings.casLoginUrl(): String = this[CAS_LOGIN_URL]?.value.toString()
// Message settings
fun PublicSettings.showDeletedStatus(): Boolean = this[SHOW_DELETED_STATUS]?.value == true
......
......@@ -9,6 +9,7 @@ import android.widget.TextView
import chat.rocket.android.widget.emoji.EmojiParser
import chat.rocket.android.widget.emoji.EmojiTypefaceSpan
import ru.noties.markwon.Markwon
import java.security.SecureRandom
fun String.ifEmpty(value: String): String {
if (isEmpty()) {
......@@ -34,6 +35,17 @@ fun EditText.erase() {
fun String.isEmailValid(): Boolean = Patterns.EMAIL_ADDRESS.matcher(this).matches()
fun generateRandomString(stringLength: Int): String {
val base = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
val secureRandom = SecureRandom()
val stringBuilder = StringBuilder(stringLength)
for (i in 0 until stringLength) {
stringBuilder.append(base[secureRandom.nextInt(base.length)])
}
return stringBuilder.toString()
}
var TextView.textContent: String
get() = text.toString()
set(value) {
......
......@@ -11,7 +11,7 @@
<android.support.constraint.ConstraintLayout
android:id="@+id/middle_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="match_parent">
<TextView
android:id="@+id/text_headline"
......@@ -21,6 +21,21 @@
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="22dp"
android:layout_marginEnd="@dimen/screen_edge_left_and_right_margins"
android:layout_marginStart="@dimen/screen_edge_left_and_right_margins"
android:layout_marginTop="64dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_headline"
tools:visibility="visible" />
<EditText
android:id="@+id/text_username_or_email"
style="@style/Authentication.EditText"
......
......@@ -4,6 +4,7 @@
<string name="title_sign_in_your_server">Faça login no seu servidor</string>
<string name="title_log_in">Entrar</string>
<string name="title_sign_up">Inscreva-se</string>
<string name="title_log_in_or_sign_up">Entrar ou Inscrever-se</string>
<string name="title_legal_terms">Termos Legais</string>
<string name="title_chats">Chats</string>
<string name="title_profile">Perfil</string>
......
......@@ -5,6 +5,7 @@
<string name="title_sign_in_your_server">Sign in your server</string>
<string name="title_log_in">Log in</string>
<string name="title_sign_up">Sign up</string>
<string name="title_log_in_or_sign_up">Log in or Sign up</string>
<string name="title_legal_terms">Legal Terms</string>
<string name="title_chats">Chats</string>
<string name="title_profile">Profile</string>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment