Commit 13837af2 authored by Tiago Cunha's avatar Tiago Cunha Committed by GitHub

Merge pull request #258 from RocketChat/feature/2fa

2FA
parents 825b0b9f 30254dde
......@@ -84,7 +84,13 @@ public class MethodCallHelper {
if (TextUtils.isEmpty(errMessageJson)) {
return Task.forError(exception);
}
String errType = new JSONObject(errMessageJson).optString("error");
String errMessage = new JSONObject(errMessageJson).getString("message");
if (TwoStepAuthException.TYPE.equals(errType)) {
return Task.forError(new TwoStepAuthException(errMessage));
}
return Task.forError(new Exception(errMessage));
} else if (exception instanceof DDPClientCallback.RPC.Error) {
String errMessage = ((DDPClientCallback.RPC.Error) exception).error.getString("message");
......@@ -214,6 +220,33 @@ public class MethodCallHelper {
});
}
public Task<Void> twoStepCodeLogin(final String usernameOrEmail, final String password,
final String twoStepCode) {
return call("login", TIMEOUT_MS, () -> {
JSONObject loginParam = new JSONObject();
if (Patterns.EMAIL_ADDRESS.matcher(usernameOrEmail).matches()) {
loginParam.put("user", new JSONObject().put("email", usernameOrEmail));
} else {
loginParam.put("user", new JSONObject().put("username", usernameOrEmail));
}
loginParam.put("password", new JSONObject()
.put("digest", CheckSum.sha256(password))
.put("algorithm", "sha-256"));
JSONObject twoStepParam = new JSONObject();
twoStepParam.put("login", loginParam);
twoStepParam.put("code", twoStepCode);
JSONObject param = new JSONObject();
param.put("totp", twoStepParam);
return new JSONArray().put(param);
}).onSuccessTask(CONVERT_TO_JSON_OBJECT)
.onSuccessTask(task -> Task.forResult(task.getResult().getString("token")))
.onSuccessTask(this::saveToken);
}
/**
* Logout.
*/
......
package chat.rocket.android.api;
public class TwoStepAuthException extends Exception {
public static final String TYPE = "totp-required";
private static final long serialVersionUID = 7063596902054234189L;
public TwoStepAuthException() {
super();
}
public TwoStepAuthException(String message) {
super(message);
}
}
......@@ -5,11 +5,12 @@ import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import chat.rocket.android.R;
import chat.rocket.android.activity.LoginActivity;
import chat.rocket.android.fragment.AbstractFragment;
import chat.rocket.android.helper.TextUtils;
abstract class AbstractServerConfigFragment extends AbstractFragment {
public static final String KEY_HOSTNAME = "hostname";
protected String hostname;
@Override
......@@ -22,7 +23,7 @@ abstract class AbstractServerConfigFragment extends AbstractFragment {
return;
}
hostname = args.getString(LoginActivity.KEY_HOSTNAME);
hostname = args.getString(KEY_HOSTNAME);
if (TextUtils.isEmpty(hostname)) {
finish();
}
......
......@@ -15,6 +15,8 @@ public interface LoginContract {
void showError(String message);
void showLoginServices(List<LoginServiceConfiguration> loginServiceList);
void showTwoStepAuth();
}
interface Presenter extends BaseContract.Presenter<View> {
......
......@@ -26,6 +26,8 @@ public class LoginFragment extends AbstractServerConfigFragment implements Login
private View btnEmail;
private View waitingView;
private TextView txtUsername;
private TextView txtPasswd;
@Override
protected int getLayout() {
......@@ -46,8 +48,8 @@ public class LoginFragment extends AbstractServerConfigFragment implements Login
@Override
protected void onSetupView() {
btnEmail = rootView.findViewById(R.id.btn_login_with_email);
final TextView txtUsername = (TextView) rootView.findViewById(R.id.editor_username);
final TextView txtPasswd = (TextView) rootView.findViewById(R.id.editor_passwd);
txtUsername = (TextView) rootView.findViewById(R.id.editor_username);
txtPasswd = (TextView) rootView.findViewById(R.id.editor_passwd);
waitingView = rootView.findViewById(R.id.waiting);
btnEmail.setOnClickListener(
view -> presenter.login(txtUsername.getText().toString(), txtPasswd.getText().toString()));
......@@ -55,7 +57,7 @@ public class LoginFragment extends AbstractServerConfigFragment implements Login
final View btnUserRegistration = rootView.findViewById(R.id.btn_user_registration);
btnUserRegistration.setOnClickListener(view -> UserRegistrationDialogFragment.create(hostname,
txtUsername.getText().toString(), txtPasswd.getText().toString())
.show(getFragmentManager(), UserRegistrationDialogFragment.class.getSimpleName()));
.show(getFragmentManager(), "UserRegistrationDialogFragment"));
}
@Override
......@@ -115,6 +117,13 @@ public class LoginFragment extends AbstractServerConfigFragment implements Login
}
}
@Override
public void showTwoStepAuth() {
showFragmentWithBackStack(TwoStepAuthFragment.create(
hostname, txtUsername.getText().toString(), txtPasswd.getText().toString()
));
}
@Override
public void onResume() {
super.onResume();
......
......@@ -8,6 +8,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import bolts.Task;
import chat.rocket.android.BackgroundLooper;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.api.TwoStepAuthException;
import chat.rocket.android.helper.Logger;
import chat.rocket.android.helper.TextUtils;
import chat.rocket.android.shared.BasePresenter;
......@@ -74,7 +75,14 @@ public class LoginPresenter extends BasePresenter<LoginContract.View>
.continueWith(task -> {
if (task.isFaulted()) {
view.hideLoader();
view.showError(task.getError().getMessage());
final Exception error = task.getError();
if (error instanceof TwoStepAuthException) {
view.showTwoStepAuth();
} else {
view.showError(error.getMessage());
}
}
return null;
});
......
package chat.rocket.android.fragment.server_config;
import chat.rocket.android.shared.BaseContract;
public interface TwoStepAuthContract {
interface View extends BaseContract.View {
void showLoading();
void hideLoading();
void showError(String message);
}
interface Presenter extends BaseContract.Presenter<View> {
void onCode(String twoStepAuthCode);
}
}
package chat.rocket.android.fragment.server_config;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.view.View;
import android.widget.TextView;
import chat.rocket.android.R;
import chat.rocket.android.api.MethodCallHelper;
public class TwoStepAuthFragment extends AbstractServerConfigFragment
implements TwoStepAuthContract.View {
private static final String ARG_USERNAME_OR_EMAIL = "usernameOrEmail";
private static final String ARG_PASSWORD = "password";
private View waitingView;
private View submitButton;
private TwoStepAuthContract.Presenter presenter;
public static TwoStepAuthFragment create(String hostname, String usernameOrEmail,
String password) {
Bundle args = new Bundle();
args.putString(AbstractServerConfigFragment.KEY_HOSTNAME, hostname);
args.putString(ARG_USERNAME_OR_EMAIL, usernameOrEmail);
args.putString(ARG_PASSWORD, password);
TwoStepAuthFragment fragment = new TwoStepAuthFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if (args == null || !args.containsKey(ARG_USERNAME_OR_EMAIL) || !args
.containsKey(ARG_PASSWORD)) {
finish();
return;
}
presenter = new TwoStepAuthPresenter(
new MethodCallHelper(getContext(), hostname),
args.getString(ARG_USERNAME_OR_EMAIL),
args.getString(ARG_PASSWORD)
);
}
@Override
public void onResume() {
super.onResume();
presenter.bindView(this);
}
@Override
public void onPause() {
presenter.release();
super.onPause();
}
@Override
public void showLoading() {
submitButton.setEnabled(false);
waitingView.setVisibility(View.VISIBLE);
}
@Override
public void hideLoading() {
waitingView.setVisibility(View.GONE);
submitButton.setEnabled(true);
}
@Override
public void showError(String message) {
Snackbar.make(rootView, message, Snackbar.LENGTH_LONG).show();
}
@Override
protected int getLayout() {
return R.layout.fragment_two_step_auth;
}
@Override
protected void onSetupView() {
waitingView = rootView.findViewById(R.id.waiting);
final TextView twoStepCodeTextView = (TextView) rootView.findViewById(R.id.two_step_code);
submitButton = rootView.findViewById(R.id.btn_two_step_login);
submitButton.setOnClickListener(view -> {
presenter.onCode(twoStepCodeTextView.getText().toString());
});
}
}
package chat.rocket.android.fragment.server_config;
import chat.rocket.android.api.MethodCallHelper;
import chat.rocket.android.shared.BasePresenter;
public class TwoStepAuthPresenter extends BasePresenter<TwoStepAuthContract.View>
implements TwoStepAuthContract.Presenter {
private final MethodCallHelper methodCallHelper;
private final String usernameOrEmail;
private final String password;
public TwoStepAuthPresenter(MethodCallHelper methodCallHelper, String usernameOrEmail,
String password) {
this.methodCallHelper = methodCallHelper;
this.usernameOrEmail = usernameOrEmail;
this.password = password;
}
@Override
public void onCode(String twoStepAuthCode) {
view.showLoading();
methodCallHelper.twoStepCodeLogin(usernameOrEmail, password, twoStepAuthCode)
.continueWith(task -> {
if (task.isFaulted()) {
view.hideLoading();
view.showError(task.getError().getMessage());
}
return null;
});
}
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorPrimaryDark">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@color/white"
android:minWidth="288dp"
android:orientation="vertical"
android:padding="@dimen/margin_24">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
style="@style/Base.TextAppearance.AppCompat.Large"
android:text="@string/two_factor_authentication_title" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Base.TextAppearance.AppCompat.Body1"
android:text="@string/open_your_authentication_app_and_enter_the_code" />
<android.support.design.widget.TextInputLayout
android:id="@+id/text_input_two_step_code"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/two_step_code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/two_factor_code"
android:imeOptions="actionNext"
android:inputType="textWebEmailAddress"
android:maxLines="1" />
</android.support.design.widget.TextInputLayout>
<Space
android:layout_width="wrap_content"
android:layout_height="@dimen/margin_16" />
<android.support.design.widget.FloatingActionButton
android:id="@+id/btn_two_step_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
app:elevation="2dp"
app:fabSize="normal"
app:srcCompat="@drawable/ic_arrow_forward_white_24dp" />
<chat.rocket.android.widget.WaitingView
android:id="@+id/waiting"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</LinearLayout>
</FrameLayout>
\ No newline at end of file
......@@ -47,4 +47,7 @@
<string name="connection_error_try_later">There\'s a connection error. Please try later.</string>
<string name="version_info_text">Version: %s</string>
<string name="two_factor_authentication_title">Two-factor authentication</string>
<string name="open_your_authentication_app_and_enter_the_code">Open your authentication app and enter the code</string>
<string name="two_factor_code">Two-factor code</string>
</resources>
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