Welcome to week nine of Mobile Systems and Applications! (cs.upt.ro/~alext/msa/lab11)
Topics of discussion
User authentication: the principles
Managing user login with Firebase
Authentication flow in Android apps
Updating the database structure to support multiple users
Most apps need to know the identity of a user. Knowing a user's identity allows an app to securely save user data in the cloud and provide the same personalized experience across all of the user's devices. So what are the standard practices to allow users to register, input their credentials (i.e. password), securely storing the credentials, and keeping users logged in? Security is a major concern today, and we will not cover the whole spectrum of problems that may occur. However, let's discuss the basics.
In terms of cryptographic methods, there are two types of encryption:
Bidirectional - information can be retrieved from encryption
Symmetric: having one password for both encryption and decryption. C = pass(M); M = pass(C)
Asymmetric: having a private-public key system, like RSA. C = M^e%n; M = C^d%n, where (M^e)^d = (M^d)^e = M%n with e (public key) and d (private key). This type of encryption can be used in two ways:
Using a target's public key to encode data and have it sent to him. Only he may decode it using his private key.
Using own private key to encode data. Everyone may decode it using the source's public key; this is useful for signing/certification.
One-directional - information cannot be retrieved from encryption
Hash functions are mathematical function which map data M or any length to a fixed-size value D called a message digest. The function D = h(M) is not reversible however, so M cannot be deduced from D, only through brute force or specialized attacks. A hash function has four important properties:
h(x) is easy to compute
h-1(x) is very hard to compute
Small modifications in X lead to consistent differences in the digest, namely X*~X, but h(X*)<<>>h(X)
It is very hard to find collisions Y such that h(Y)==h(X)
If you receive an email asking you to change your password on a specific site, and that email contains your password in plain text, it is a very alarming email! The paradox is that sites like Yahoo, Google, Facebook and others, should not have your passwords stored in plain text on their servers, yet they should be able to authenticate you at any moment. This seemingly impossible magic trick is done using hash functions. When a user registers, they send their username (email, name etc.) and the hashed password over a reliable connection to the server. As such, the server never knows the original plain text password the user chose, so no site administrator could sniff passwords, and no hacker may use the data if the database is compromised. Next, whenever the user logs back in, he will input the password, and that will be hashed locally and sent to the server. If the password is correct, then its hash will coincide with the one originally stored.
A hash function works similar to a modulo function, but it may not simply truncate data in order to make it fit a predefined 256 or 512 bit space. An example function like:
h(x) = (x^2 + 2*x - 5) % 16
has the properties that it fits the output h(x) inside the interval [0,16] for any given x; it is computed fast; it is impossible to reverse because there are infinite collisions; but has the big problem of having many obvious collisions. We can find many values for x for which h(x1)==h(x2)==h(x3) etc. Hash functions may be designed keeping the mathematical aspects in mind, however, it is not advisable for anyone - including computer engineers - to invent new hash functions. Thus is the job of mathematicians, crypto-experts and years of industry validation. Why? Because proving the four properties of hash functions is a very complex task. As such, rely on libraries implementing standard hash functions like MD2,4,5,6, SHA-256,-384,-512, SHA-3, and others. A relatively newer version of the standard MD5 (since 1992) is MD6 (since 2008) which comes with a length of 512 bits and is a keyed function.
The downside of using an industry standard like MD5 is that it has been around for over a decade or two. In this time, attackers have been constantly running the functions on arbitrary data and have stored these results in giant lookup tables. In other words, if today an attacker intercepts your password, they will do two things:
A dictionary attack: a complex brute-force attack which uses smart combinations of common words from a dictionary (like English) and checks all combinations of words before going into the random brute force attacks. If your password is a combination of plain English words, then the number of tries for a dictionary attack will be reduced from trillions to millions of tries. With the right parallel-efficient hardware (like a GPU), a weak password can be broken in a matter of minutes to hours.
A lookup table which stores all combinations of inputs (e.g. from 0000 to FFFF) mapped to their resulting hash. This makes the breaking trivial by just looking up a hash result in the table.
To overcome this limitation, the concept of salts is employed. Salts are long random strings which are appended to the password before hashing. In this case, the lookup table or dictionary attacks are avoided.
No salt:
d = h("password")
Digest d is the same for all users with "password", so it is prone to dictionary and lookup table attacks.
With salt:
salt1 = "gfODoOJWKttC5oTBinmu"
d1 = h("password"+ salt1)
salt2 = "hjICz1nVTifRSORX3aQw"
d2 = h("password"+ salt2)
Digets d1 and d2 are different for users 1 and 2, and different from d, so they are not prone to dictionary and lookup table attacks.
Whenever a user creates a password, he receives a random string salt which is appended to his (possibly weak) password. The salt must be stored alongside the hash on the server. When the user login back in, he must first retrieve the salt from the server, then compute the hash locally, and send it to the server for authentication. If the salt is compromised, no harm can be done because of the 3rd and 4th properties of hash functions. In conclusion, the strategy for registering and authentication is given below:
Register
Create password M and generate salt S.
Compute D=h(M+S).
Send D and S to server for safe keeping.
Log-in
Obtain S from server.
Type password M and compute D=h(M+S).
Send D for matching on server.
Firebase Authentication provides backend services, easy-to-use SDKs, and ready-made UI libraries to authenticate users to your app. It supports authentication using passwords, popular federated identity providers like Google, Facebook and Twitter, and more. The provided authentication integrates tightly with other Firebase services, and it leverages industry standards like OAuth 2.0 and OpenID Connect, so it can be easily integrated with your custom backend.
To sign a user into your app, you first get authentication credentials from the user. These credentials can be the user's email address and password, or an OAuth token from a federated identity provider. Then, you pass these credentials to the Firebase Authentication SDK. The backend services will then verify those credentials and return a response to the client. After a successful sign in, you can access the user's basic profile information, and you can control the user's access to data stored in other Firebase products. You can also use the provided authentication token to verify the identity of users in your own backend services.
A Firebase user object represents the account of a user who has signed up to an app in a Firebase project. Apps usually have many registered users, and every app in a Firebase project shares a user database. A user has a fixed set of basic properties: a unique ID, a primary email address, a name and a photo URL. These are stored in the project's user database, that can be updated by the user (iOS, Android, web). You cannot add other properties to the Firebase User object directly; instead, you can store the additional properties in your real-time database.
The first time a user signs up to your app, the user's profile data is populated using the available information. If the user signed up with:
An email address and password, only the primary email address property is populated.
A federated identity provider, such as Google or Facebook, the account information made available by the provider is used to populate the Firebase User's profile.
Your custom authentication system, you must explicitly add the information you want to the Firebase User's profile.
Once a user account has been created, you can reload the user's information to incorporate any changes the user might have made on another device.
You can sign in users to your Firebase apps using several methods: email address and password, federated identity providers, and your custom auth system, as depicted in Figure 1. You can associate more than one sign-in method with a user: for example, a user can sign in to the same account using an email address and a password, or using Google Sign-In. A user instance keeps track of every provider linked to the user. This allows you to update empty profile's properties using the information given by a provider. See Managing Users for Android.
Fig.1.
When a user signs up or signs in, that user becomes the current user of the Auth instance. The Firebase Auth instance persists the user's state, so that refreshing the page (in a browser) or restarting the application doesn't lose the user's information. When the user signs out, the Auth instance stops keeping a reference to the user object and no longer persists its state; there is no current user. However, the user instance continues to be completely functional: if you keep a reference to it, you can still access and update the user's data.
The recommended way to track the current state of the Firebase Auth instance is by using listeners. An Auth listener gets notified any time something relevant happens to the Auth object. More details on Android are available here. An auth listener gets notified in the following situations:
The Auth object finishes initializing and a user was already signed in from a previous session, or has been redirected from an identity provider's sign-in flow.
A user signs in (the current user is set).
A user signs out (the current user becomes null).
The current user's access token is refreshed. This case can happen in the following conditions:
The access token expires: this is a common situation. The refresh token is used to get a new valid set of tokens.
The user changes his password: Firebase issues new access and refresh tokens and renders the old tokens expired. This automatically signs out the user on every device, for security reasons.
The user re-authenticates: some actions require that the user's credentials are recently issued; such actions include deleting an account, setting a primary email address, and changing a password. Instead of signing out the user and then signing in the user again, get new credentials from the user, and pass the new credentials to the reauthenticate method of the user object.
The rest of the tutorial of how to properly implement authentication using Firebase is described in Task 1, which proposes to change the Smart Wallet application as well as real-time database structure. For detailed explanations make sure you read the documentation.
The login and registering processes are important use cases for any Android app. Equally important is ensuring the design of the app allows the user to bypass authentication once the password is saved or a valid token is acquired. In other words, you wouldn't want to force the user to input his username and password every time he or she opens their email or chat clients. As long as users do not sign out, they should be kept authenticated. A direct approach would seem to create a login activity which checks for a saved password, then automatically starts the main activity. While functionally correct, this flow makes the activity stack have an unexpected structure. Specifically, the main activity will be put on top of the login activity, which is counter-intuitive, since if the user taps the back button, he will not expect to navigate back to the login activity which redirects him back in an infinite loop. As such, we introduce the concept of starting activity for a result.
An activity may be started using the method startActivityForResult(intent, requestCode). The second activity is started with the goal of obtaining some user input (e.g. username & password, profile picture, or a calendar date) then returns that input with a success code, or an error code otherwise. As such, when the second activity has the necessary data it will call the following sequence of code.
Start from MainActivity
private static final int REQ_SIGNIN = 3;
startActivityForResult(new Intent(getApplicationContext(), SignupActivity.class), REQ_SIGNIN);
Return result from SignupActivity
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
intent.putExtra("user", user.getEmail());
intent.putExtra("pass", pass);
setResult(RESULT_OK);
finish();
Retrieve results in MainActivity
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQ_SIGNIN) {
if (resultCode == RESULT_OK) {
// get data from intent
String user = data.getStringExtra("user");
String pass = data.getStringExtra("pass");
// ...
} else if (resultCode == RESULT_CANCELED) {
// data was not retrieved
}
}
}
When an activity starts another one with the startActivityForResult method, it uses the request code (integer) to determine if it gets a result back whrn it is restarted. To that end, you have to overwrite the onActivityResult method which returns the following:
request code: integer code with which the returning activity was started
result code: a predefined (OK, CANCELED) or custom integer code which is specified by the started activity. It tells us if the started activity succeeded in getting the user input or not.
data: the intent in which the started activity may encapsulate data to be returned.
On application startup, the main activity should check if the user is logged in. If yes, remain in the main activity and proceed with the rest of the application logic (e.g. get data from server). If not, then start a second activity meant for getting the user name and password from the user. In case the user is not registered, start another activity for getting more profile information (e.g. display name, address, phone number etc). Once registered return to the login activity and automatically log in. Once logged in, automatically return the auth token to the main activity and proceed with the rest of the application logic.
The current structure of the Smart Wallet application is such that it defines all payment entries under the 'wallet' node. This current structure does not allow us to assign some payments to a specific user, and other payments to another user. There are two solutions to enable the multi-user support. The first would be to add a field to each payment specifying the user id of the owner. An example is given below in Figure 2.
Fig.2.
Once a user id (uid) of type string is added to each payment, we would need to obtain the uid of the currently logged in user. However, if we are granted access to a another user's payments (share feature), there would need to be a list of all user ids so we know what uid to search after. This is the classic SQL approach, where we use the uid as a foreign key. Nevertheless, in the spirit of Firebase, another approach is more intuitive. Instead of adding foreign keys to payments, we can structure all payments to be inside their respective user folder. Figure 3 shows the database structure before and after modification.
Fig.3.
This approach is considered to flatten the data structure so that it can be efficiently processed without the need to iterate through all payments and filter them by uid. Also, the previous method would require setting a listener on the whole data structure, namely our listener would get triggered by any modification done by any user. Remember, this structure is important as we cannot write server-side code, so we need to be smart. In this case, when the app starts, a Firebase listener will be attached on the node given by the user id, namely:
databaseReference.child("wallet").child(uid).addChildEventListener(new ChildEventListener() { ... });
All reads and writes will be done under the currently logged in user node, defined by its user id. To get the Firebase uid, simply call user.getUid().
Based on the Smart Wallet application from last week, implement the functionality to allow users to register, log in, sign out and have restricted access only to their own payments which they create, edit and delete.
First, we need to enable the type of login supported by our application. In the Firebase console, go to authentication and enable Email/Password authentication, as depicted in Figure 4. Additionally, here is where you may opt for Google or any other type of providers.
Fig.4.
Add an authentication listener in your MainActivity to listen to any changes in the state of the user. The listener created in onCreate will fire when it is attached, and then every time a user logins in, out or his token expires. The triggered method returns the currently logged in user (or null). If using the generic email/password sign-in method, then the user class can be polled for email and uid, which we will both need later on. If the received user is null then it means the auth token is invalid and the current user must login (or register). This will start the second activity designed especially for this.
// Firebase authentication
private FirebaseAuth mAuth;
private FirebaseAuth.AuthStateListener mAuthListener;
private static final int REQ_SIGNIN = 3;
@Override
protected void onCreate(Bundle savedInstanceState) {
// setup authentication
mAuth = FirebaseAuth.getInstance();
mAuthListener = new FirebaseAuth.AuthStateListener() {
@Override
public void onAuthStateChanged(FirebaseAuth firebaseAuth) {
FirebaseUser user = firebaseAuth.getCurrentUser();
if (user != null) {
TextView tLoginDetail = (TextView) findViewById(R.id.tLoginDetail);
TextView tUser = (TextView) findViewById(R.id.tUser);
tLoginDetail.setText("Firebase ID: " + user.getUid());
tUser.setText("Email: " + user.getEmail());
AppState.get().setUserId(user.getUid());
attachDBListener(user.getUid());
} else {
startActivityForResult(new Intent(getApplicationContext(), SignupActivity.class), REQ_SIGNIN);
}
}
};
}
The authentication listener must be registered and unregistered every time the main activity starts and stops. As such, add the following code to your MainActivity.
@Override
public void onStart() {
super.onStart();
mAuth.addAuthStateListener(mAuthListener);
}
@Override
public void onStop() {
super.onStop();
if (mAuthListener != null) {
mAuth.removeAuthStateListener(mAuthListener);
}
}
A important observation is that now we have a two-step non-synchronous process: the activity starts, creates a listener and waits for the authentication event to fire, then registers the database listener and waits for payment data to be retrieved. As such, if we attach the database listener at startup (onCreate) and runs prior to the authentication, then no data will be retrieved (i.e. the user is not yet authenticated). To overcome this synchronization problem, we attach the database listener only when we get a user that is not null in the onAuthStateChangedMethod. We simply move the code we had before to another method outside onCreate, and call it when necessary.
private void attachDBListener(String uid) {
// setup firebase database
final FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference databaseReference = database.getReference();
AppState.get().setDatabaseReference(databaseReference);
databaseReference.child("wallet").child(uid).addChildEventListener(new ChildEventListener() {
//...
});
}
Finally, make sure you change all calls to updateChildren and removeValue to match the correct path to the user node. For this, we need to store the current uid in the AppState. Don't forget to also store it once you have it ( AppState.get().setUserId(user.getUid()) )!
String user = AppState.get().getUserID();
AppState.get().getDatabaseReference().child("wallet").child(user).child(timestamp).updateChildren(map);
If the user is not signed in (user == null) then we start a second activity, called SignupActivity, dedicated for getting the user name and password from the user. This activity is started with the request code REQ_SIGNIN = 3.
Create an empty activity named SignupActivity which may look like the one suggested in Figure 5. The code for the xml layout is given here. The activity will register its own authentication listener and check if the user is signed in. Clicking on the sign in or register buttons will trigger the according Firebase calls: mAuth.signInWithEmailAndPassword(email, password) and mAuth.createUserWithEmailAndPassword(email, password).
Fig.5.
The user should remain on this screen (i.e. in this activity) until the received user is not null. Only when the authentication listener is triggered and we receive a non-null user does it mean we are logged in. In this case, this activity should finish and we will return to the MainActivity. The code skeleton for the SignupActivity is given here.
Homework: Enable a third party sign-up method of your choice: Google, Facebook, Twitter or Github. Make the necessary modifications to the UI of SignupActivity (e.g. add a Google logo button). Plus one activity (+1) point for the homework. Homework may only be presented individually, on your smartphone or emulator, with the code running on your PC.
Have a fruitful week.
Mangosteen fruit
The Mangosteen plant is a tropical evergreen tree, and it was originated in the Sunda Islands and the Moluccas. It is a dark purple fruit with white on the inside of the fruit. Even though mangosteen sounds like a mango type, it is really very different from mango. Mangosteen is a delectably sweet and juicy fruit that offers numerous health benefits of both the fruit itself and its skin which are incredibly potent disease fighters. Delicious as it is useful, the mangosteen fruit is always rich in xanthones, which may promote healthy physical function. In addition, a mangosteen serving contains up to 5 grams of fiber. Its purplish pigment is also used as a dye.