/*
* Copyright (C) 2014 Divide.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.divide.client.auth;
import com.google.inject.Inject;
import io.divide.client.BackendUser;
import io.divide.client.Config;
import io.divide.client.auth.credentials.LocalCredentials;
import io.divide.client.auth.credentials.LoginCredentials;
import io.divide.client.auth.credentials.SignUpCredentials;
import io.divide.client.auth.credentials.ValidCredentials;
import io.divide.client.data.ServerResponse;
import io.divide.client.http.Status;
import io.divide.client.web.AbstractWebManager;
import io.divide.shared.logging.Logger;
import io.divide.shared.transitory.Credentials;
import io.divide.shared.util.Crypto;
import io.divide.shared.util.ObjectUtils;
import org.jetbrains.annotations.NotNull;
import rx.Observable;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
import java.security.PublicKey;
import java.util.List;
import java.util.Map;
import static io.divide.client.auth.LoginState.*;
public class AuthManager extends AbstractWebManager<AuthWebService> {
private static Logger logger = Logger.getLogger(AuthManager.class);
private BackendUser user;
private AccountStorage accountStorage;
private LoginState CURRENT_STATE = LoginState.LOGGED_OUT;
private PublishSubject<BackendUser> loginEventPublisher = PublishSubject.create();
@Inject
public AuthManager(Config config, AccountStorage accountStorage) {
super(config);
this.accountStorage = accountStorage;
loadCachedUser().subscribeOn(Schedulers.io()).subscribe(new Action1<BackendUser>() {
@Override
public void call(BackendUser user) { }
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
logger.error("Failed to login", throwable);
}
});
}
private Observable<BackendUser> loadCachedUser(){
logger.debug("Stored user Login: " + getStoredAccount());
if(getUser() != null) return Observable.from(getUser());
else if(getStoredAccount() != null){
logger.debug("Stored user Login: " + getStoredAccount());
return guardedLogin(getStoredAccount());
}
else return Observable.empty();
}
/**
* Return LocalCredentials representing logged in user credentials stored to disk.
* @return locally stored credentials. Representing account name and access token
*/
public LocalCredentials getStoredAccount(){
return ObjectUtils.get1stOrNull(accountStorage.getAccounts());
}
private Observable<BackendUser> guardedLogin(LocalCredentials account){
synchronized (AuthManager.class){
switch (CURRENT_STATE){
case LOGGED_IN: return Observable.from(getUser()); // already logged in, return user
case LOGGING_IN: return loginEventPublisher.asObservable(); // already loggin in, listen to result
case LOGGED_OUT: return getUserFromAuthToken(account.getAuthToken()); // asked to login, we arnt, so login
default: return Observable.error(new Exception("Invalid current State?!: " + CURRENT_STATE));
}
}
}
/**
* Login using user authentication token
* @param authToken authentication user for user.
* @return Logged in user.
*/
public Observable<BackendUser> getUserFromAuthToken(final String authToken){
return Observable.create(new Observable.OnSubscribe<BackendUser>() {
@Override
public void call(Subscriber<? super BackendUser> subscriber) {
try{
setLoginState(LOGGING_IN);
logger.debug("getWebService(): " + getWebService());
logger.debug("getuserFromAuthToken: " + authToken);
ValidCredentials validCredentials = getWebService().getUserFromAuthToken(authToken).toBlocking().first();
if(validCredentials == null) throw new Exception("Null User Returned");
subscriber.onNext(setUser(validCredentials));
}catch (Exception e){
setLoginState(LOGGED_OUT);
subscriber.onError(e);
}
}
});
}
/**
* Login using user recovery token
* @param recoveryToken recovery token for user.
* @return Logged in user.
*/
public Observable<BackendUser> getUserFromRecoveryToken(final String recoveryToken){
return Observable.create(new Observable.OnSubscribe<BackendUser>() {
@Override
public void call(Subscriber<? super BackendUser> subscriber) {
try{
setLoginState(LOGGING_IN);
ValidCredentials validCredentials = getWebService().getUserFromRecoveryToken(recoveryToken).toBlockingObservable().first();
if(validCredentials == null) throw new Exception("Null User Returned");
subscriber.onNext(setUser(validCredentials));
}catch (Exception e){
setLoginState(LOGGED_OUT);
subscriber.onError(e);
}
}
});
}
/**
* Returns currently logged in user if one exists.
* @return Logged in user or null.
*/
public BackendUser getUser() {
return user;
}
/**
* Log out current user if logged in.
*/
public void logout(){
List<LocalCredentials> accountList = accountStorage.getAccounts();
if(accountList.size() == 1){
String userName = accountList.get(0).getName();
logger.debug("logout: " + userName);
accountStorage.removeAccount(userName);
user = null;
}
setLoginState(LoginState.LOGGED_OUT);
}
private BackendUser setUser(ValidCredentials returned){
logger.debug("setUser: " + returned);
user = BackendUser.from(returned);
logger.debug("setUser: " + getUser());
if(returned!=null) storeOrUpdateAccount(returned);
fireLoginListeners();
setLoginState(LOGGED_IN);
return getUser();
}
private void storeOrUpdateAccount(Credentials validCredentials){
logger.debug("storeOrUpdateAccount: " + validCredentials);
String accountName = validCredentials.getEmailAddress();
String recoveryToken = validCredentials.getRecoveryToken();
String authToken = validCredentials.getAuthToken();
if (!accountStorage.exists(accountName)) {
LocalCredentials account = new LocalCredentials();
account.setName(accountName);
account.setAuthToken(authToken);
account.setRecoveryToken(recoveryToken);
accountStorage.addAcccount(account);
} else {
accountStorage.setAuthToken(accountName,authToken);
if(recoveryToken!=null && recoveryToken.length()>0)
accountStorage.setRecoveryToken(accountName, recoveryToken);
}
}
private static PublicKey serverPublicKey = null;
/**
* Returns server public key. Queries server or local copy.
* @return Server public key.
*/
public PublicKey getServerKey(){
logger.debug("getServerKey()");
try {
if(serverPublicKey!=null) return serverPublicKey;
byte[] pubKey = getWebService().getPublicKey();
logger.debug("pubKey: " + String.valueOf(pubKey));
serverPublicKey = Crypto.pubKeyFromBytes(pubKey);
return serverPublicKey;
} catch (Exception e) {
logger.error("Failed to getServerKey()", e);
}
return null;
}
/**
* Syncronously attempt to create user account
* @param loginCreds Credentials used to create account.
* @return Response of the operation
*/
public SignUpResponse signUp(SignUpCredentials loginCreds){
logger.debug("signUp(" + loginCreds + ")");
try {
setLoginState(LOGGING_IN);
PublicKey key = Crypto.pubKeyFromBytes(getWebService().getPublicKey());
loginCreds.encryptPassword(key);
logger.debug("Login Creds: " + loginCreds);
ServerResponse<ValidCredentials> response = ServerResponse.from(ValidCredentials.class, getWebService().userSignUp(loginCreds));
logger.debug("Response: " + response.getStatus());
BackendUser user;
if (response.getStatus().isSuccess()) {
user = setUser(response.get());
} else {
return new SignUpResponse(null, Status.SERVER_ERROR_INTERNAL, " null user returned");
}
return new SignUpResponse(user, response.getStatus(), response.getError());
} catch (Exception e) {
logger.error("SignUp Failure(" + loginCreds.getEmailAddress() + ")", e);
return new SignUpResponse(null, Status.SERVER_ERROR_INTERNAL, e.getLocalizedMessage());
}
}
/**
* Asyncronously attempt to create user account
* @param signInCreds Credentials used to create account.
* @return Response of the operation
*/
public Observable<BackendUser> signUpASync(final SignUpCredentials signInCreds){
logger.debug("signUpASync("+signInCreds+")");
try {
setLoginState(LOGGING_IN);
return getWebService().getPublicKeyA().flatMap(new Func1<byte[], Observable<SignUpCredentials>>() {
@Override
public Observable<SignUpCredentials> call(byte[] bytes) {
try {
PublicKey key = Crypto.pubKeyFromBytes(bytes);
signInCreds.encryptPassword(key);
return Observable.from(signInCreds);
}catch (Exception e) {
return Observable.error(e);
}
}
}).flatMap(new Func1<SignUpCredentials, Observable<ValidCredentials>>() {
@Override
public Observable<ValidCredentials> call(SignUpCredentials o) {
return getWebService().userSignUpA(signInCreds);
}
}).map(new Func1<ValidCredentials, BackendUser>() {
@Override
public BackendUser call(ValidCredentials validCredentials) {
return setUser(validCredentials);
}
})
.subscribeOn(Schedulers.io()).observeOn(config.observeOn());
} catch (Exception e) {
logger.error("Failed to signUp(" + signInCreds.getEmailAddress() + ")", e);
return Observable.error(e);
}
}
/**
* Syncronously attempt to log into user account
* @param loginCreds Credentials used to login.
* @return Response of the operation
*/
public SignInResponse login(final LoginCredentials loginCreds){
logger.debug("login("+loginCreds+")");
try{
setLoginState(LOGGING_IN);
if(!loginCreds.isEncrypted()){
PublicKey key = Crypto.pubKeyFromBytes(getWebService().getPublicKey());
loginCreds.encryptPassword(key);
}
logger.debug("Login Creds: " + loginCreds);
ServerResponse<ValidCredentials> response = ServerResponse.from(ValidCredentials.class,getWebService().login(loginCreds));
BackendUser user;
if(response.getStatus().isSuccess()){
user = setUser(response.get());
} else {
logger.error("Login Failure("+loginCreds.getEmailAddress()+"): " + response.getStatus().getCode() + " " + response.getError());
setLoginState(LOGGED_OUT);
return new SignInResponse(null, Status.SERVER_ERROR_INTERNAL,"Login failed");
}
return new SignInResponse(user,response.getStatus(),response.getError());
}catch (Exception e){
logger.error("Login Failure("+loginCreds.getEmailAddress()+")",e);
setLoginState(LOGGED_OUT);
return new SignInResponse(null, Status.SERVER_ERROR_INTERNAL,e.getLocalizedMessage());
}
}
/**
* Asyncronously attempt to log into user account
* @param loginCreds Credentials used to login.
* @return Response of the operation
*/
public Observable<BackendUser> loginASync(final LoginCredentials loginCreds){
logger.debug("loginASync("+loginCreds+")");
try{
setLoginState(LOGGING_IN);
return getWebService().getPublicKeyA().flatMap(new Func1<byte[], Observable<LoginCredentials>>() {
@Override
public Observable<LoginCredentials> call(byte[] bytes) {
try {
if (!loginCreds.isEncrypted()) {
PublicKey key = Crypto.pubKeyFromBytes(bytes);
loginCreds.encryptPassword(key);
}
return Observable.from(loginCreds);
} catch (Exception e) {
return Observable.error(e);
}
}
}).flatMap(new Func1<LoginCredentials, Observable<ValidCredentials>>() {
@Override
public Observable<ValidCredentials> call(LoginCredentials credentials) {
return getWebService().loginA(credentials);
}
}).map(new Func1<ValidCredentials, BackendUser>() {
@Override
public BackendUser call(ValidCredentials validCredentials) {
return setUser(validCredentials);
}
})
.subscribeOn(Schedulers.io()).observeOn(config.observeOn());
}catch (Exception e){
logger.error("Failed to SignIn("+loginCreds.getEmailAddress()+")",e);
setLoginState(LOGGED_OUT);
return Observable.error(e);
}
}
private void setLoginState(@NotNull LoginState state){
logger.debug("Changing State("+config.id+"): " + state);
CURRENT_STATE = state;
}
/**
* Update remote server with new user data.
* @return status of update.
*/
public Observable<Void> sendUserData(BackendUser backendUser){
return getWebService().sendUserData(isLoggedIn(),backendUser.getOwnerId()+"",backendUser.getUserData())
.subscribeOn(config.subscribeOn()).observeOn(config.observeOn());
}
/**
* Query server to return user data for the logged in user
* @return updated BackendUser.
*/
public Observable<Map<String,Object>> getUserData(BackendUser backendUser){
return getWebService().getUserData(isLoggedIn(), backendUser.getOwnerId() + "")
.subscribeOn(config.subscribeOn()).observeOn(config.observeOn());
}
@Override
protected Class<AuthWebService> getType() {
return AuthWebService.class;
}
/**
* Add loginListener to listen to login events
* @param listener LoginListener to receive events.
*/
public void addLoginListener(LoginListener listener){
logger.debug("addLoginListener");
Subscription subscription = loginEventPublisher
.subscribeOn(config.subscribeOn())
.observeOn(config.observeOn())
.subscribe(listener);
listener.setSubscription(subscription);
}
/**
* @hide
*/
private void fireLoginListeners(){
logger.debug("fireLoginListeners: " + getUser());
loginEventPublisher.onNext(getUser());
}
/**
* Used to determine if a user is logged in localy, if not remote operations can not be performed.
* @return authentication token for logged in user.
* @throws RuntimeException Execption thrown if if remote operaton is requested but no user is logged in.
*/
private String isLoggedIn() throws RuntimeException {
if(this.getUser() != null && this.getUser().getAuthToken() != null){
return "CUSTOM " + this.getUser().getAuthToken();
} else {
throw new RuntimeException("User state error.");
}
}
}