Skip to content

Instantly share code, notes, and snippets.

@aakashns
Last active November 4, 2016 01:06
Show Gist options
  • Select an option

  • Save aakashns/ee1e977cf169a1460a84bf6f168d3464 to your computer and use it in GitHub Desktop.

Select an option

Save aakashns/ee1e977cf169a1460a84bf6f168d3464 to your computer and use it in GitHub Desktop.
Helper class to connect Facebook's AccountKit with Firebase to create OTP (SMS) based login flow on Android
package in.swiftace.firekit;
import android.content.Intent;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import com.facebook.accountkit.AccountKitLoginResult;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
/**
* SMS One-time-password(OTP) based login flow for Firebase using Facebook's AccountKit.
* Demo: https://youtu.be/Ja9F71Ih1L4
*
* AccountKit and Firebase are great, but it takes some work to make them
* work well together. This is a helper class to use the authorization code
* returned by AccountKit to create a new Firebase user using custom
* authentication token mechanism.
*
* Requires server side code to retrieve the user ID from AccountKit
* and to create a Firebase authentication token for logging in at client side.
*
* References:
* AccountKit for Android - https://developers.facebook.com/docs/accountkit/android
* Firebase Custom Auth for Android - https://firebase.google.com/docs/auth/android/custom-auth
* Firebase Server setup - https://firebase.google.com/docs/auth/server/create-custom-tokens
*
*/
public class FirebaseAccountKit {
private static final String TAG = "FirebaseAccountKit";
private ResultListener mResultListener; // Invokes callbacks provided by the calling activity
private AccountKitLoginResult mLoginResult; // Result from AccountKit
/** Error codes for failure cases */
public static final int ACCOUNT_KIT_LOGIN_ERROR = 100;
public static final int ACCOUNT_KIT_LOGIN_CANCELLED = 101;
public static final int SERVER_AUTH_REQUEST_FAILED = 102;
public static final int SERVER_AUTH_RESPONSE_INVALID = 103;
public static final int SERVER_AUTH_RESPONSE_MISSING_TOKEN = 104;
public static final int FIREBASE_CUSTOM_AUTH_FAILED = 105;
/**
* This method is supposed to be called in onActivityResult to handle the result from
* AccountKit login flow. It sends a GET request to the authentication server with the
* URL query parameter 'authorization_code'. And it expects the server to return a JSON
* response with an object containing a key 'firebase_token', which is the custom token
* used for custom auth in Firebase. It can also contain other keys like 'id' for AccountKit
* ID and 'access_token' for AccountKit Graph API requests. E.g.
* {
* "firebase_token": "asdfsdflkahglsfklsdjfsdkf",
* "id": "324234234",
* "access_token": "sdfsdfsdfa"
* }
*
* @param data The intent received in onActivityResult
* @param serverUrl URL for the authentication server.
* @return Returns the same object to chain calls to setResultListener
*/
public FirebaseAccountKit handleAccountKitResult(Intent data, String serverUrl) {
mLoginResult = data.getParcelableExtra(AccountKitLoginResult.RESULT_KEY);
if (mLoginResult.getError() != null) {
if (mResultListener != null) mResultListener.onFirebaseAccountKitFailure(
ACCOUNT_KIT_LOGIN_ERROR, mLoginResult, "",
new Exception(getDescription(ACCOUNT_KIT_LOGIN_ERROR)));
} else if (mLoginResult.wasCancelled()) {
if (mResultListener != null) mResultListener.onFirebaseAccountKitFailure(
ACCOUNT_KIT_LOGIN_CANCELLED, mLoginResult, "",
new Exception(getDescription(ACCOUNT_KIT_LOGIN_CANCELLED)));
} else {
// AccountKit success! Get the authorization code and send to server.
String authorizationCode = mLoginResult.getAuthorizationCode();
String url = serverUrl + "?authorization_code=" + authorizationCode;
new GetUrlTask().execute(url);
}
return this;
}
/** Helper method to set the ResultListener **/
public FirebaseAccountKit handleAccountKitResult(
Intent data, String serverUrl, ResultListener resultListener) {
mResultListener = resultListener;
return handleAccountKitResult(data, serverUrl);
}
/** Set the result listener **/
public FirebaseAccountKit setResultListener(ResultListener resultListener) {
mResultListener = resultListener;
return this;
}
/**
* Handles the response of the auth server and performs Firebase login
* @param result String response from the auth server (expected to be JSON)
*/
private void performFirebaseLogin(final String result) {
if (result == null) {
if (mResultListener != null) mResultListener.onFirebaseAccountKitFailure(
SERVER_AUTH_REQUEST_FAILED, mLoginResult, "",
new Exception(getDescription(SERVER_AUTH_REQUEST_FAILED)));
return;
}
try {
final JSONObject resultJson = new JSONObject(result);
String firebaseToken = resultJson.getString("firebase_token");
if (firebaseToken == null) {
if (mResultListener != null) mResultListener.onFirebaseAccountKitFailure(
SERVER_AUTH_RESPONSE_MISSING_TOKEN, mLoginResult, result,
new Exception(getDescription(SERVER_AUTH_RESPONSE_MISSING_TOKEN)));
return;
}
// Successfully retrieved the firebase token. Now authenticate using it!
FirebaseAuth.getInstance()
.signInWithCustomToken(firebaseToken)
.addOnSuccessListener(new OnSuccessListener<AuthResult>() {
@Override
public void onSuccess(AuthResult authResult) {
// Phew! Logged in successfully.
if (mResultListener != null) mResultListener.onFirebaseAccountKitSuccess(
mLoginResult, resultJson, authResult);
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
if (mResultListener != null) mResultListener.onFirebaseAccountKitFailure(
FIREBASE_CUSTOM_AUTH_FAILED, mLoginResult, result, e);
}
});
} catch (JSONException e) {
if (mResultListener != null) {
mResultListener.onFirebaseAccountKitFailure(
SERVER_AUTH_RESPONSE_INVALID, mLoginResult, result, e);
}
}
}
/**
* Returns a string description of the error code in case of failure
* @param errorCode
* @return
*/
public static String getDescription(int errorCode) {
switch (errorCode) {
case ACCOUNT_KIT_LOGIN_CANCELLED:
return "ACCOUNT_KIT_LOGIN_CANCELLED";
case ACCOUNT_KIT_LOGIN_ERROR:
return "ACCOUNT_KIT_LOGIN_ERROR";
case SERVER_AUTH_REQUEST_FAILED:
return "SERVER_AUTH_REQUEST_FAILED";
case SERVER_AUTH_RESPONSE_INVALID:
return "SERVER_AUTH_RESPONSE_INVALID";
case SERVER_AUTH_RESPONSE_MISSING_TOKEN:
return "SERVER_AUTH_RESPONSE_MISSING_TOKEN";
case FIREBASE_CUSTOM_AUTH_FAILED:
return "FIREBASE_CUSTOM_AUTH_FAILED";
default:
return "UNKNOWN";
}
}
/**
* Callbacks invoked in case of successful login or failure. The calling activity
* should provide an implementation of this interface using .setResultListener or
* .performFirebaseLogin to appropriately handle success or failure.
*/
public interface ResultListener {
/**
* Callback for successful Firebase login
* @param accountKitLoginResult Result from AccountKit
* @param authServerResult JSON result from server
* @param firebaseAuthResult AuthResult from Firebase
*/
void onFirebaseAccountKitSuccess(
AccountKitLoginResult accountKitLoginResult,
JSONObject authServerResult,
AuthResult firebaseAuthResult);
/**
* Callback for login failure
* @param errorCode Error code indicating the source of the error
* @param accountKitLoginResult Result from AccountKit
* @param authServerResult String result from server (not JSON)
* @param e Exception (could be from Firebase depending on error code)
*/
void onFirebaseAccountKitFailure(
int errorCode,
AccountKitLoginResult accountKitLoginResult,
String authServerResult,
Exception e);
}
/**
* AsyncTask to fetch the Firebase authentication token from your server.
* Uses HttpURLConnection to avoid any external dependencies. Far too much
* code for a simple GET, no?
*/
private class GetUrlTask extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... args) {
if (args.length < 1) return null;
InputStream is = null;
try {
URL url = new URL(args[0]);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(10000);
conn.setConnectTimeout(15000);
conn.setRequestMethod("GET");
conn.setDoInput(true);
conn.connect();
is = conn.getInputStream();
return convertStreamToString(is);
} catch (MalformedURLException e) {
return null;
} catch (IOException e) {
return null;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException ignored) {
}
}
}
}
@Override
protected void onPostExecute(String result) {
performFirebaseLogin(result);
}
private String convertStreamToString(InputStream is) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
try {
while ((line = reader.readLine()) != null) {
sb.append(line).append('\n');
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}
}
}
package in.swiftace.firekit;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;
import com.facebook.accountkit.AccountKit;
import com.facebook.accountkit.AccountKitLoginResult;
import com.facebook.accountkit.ui.AccountKitActivity;
import com.facebook.accountkit.ui.AccountKitConfiguration;
import com.facebook.accountkit.ui.LoginType;
import com.google.firebase.auth.AuthResult;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import org.json.JSONObject;
/**
* Sample activity demonstrating the usage of FirebaseAccountKit.
* Assumes that you have already properly set up Firebase and
* AccountKit for your app, and that you have a running server
* for performing custom auth using Firebase.
*
* This activity implements FirebaseAccountKit.ResultListener to listen
* for the result of the AccountKit + Firebase login flow.
*/
public class AccountKitLoginActivity extends AppCompatActivity implements FirebaseAccountKit.ResultListener {
/** Server endpoint to create an account and retrieve a firebase auth token **/
private static final String SERVER_URL = "<YOUR_SERVER_URL_HERE>";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_account_kit_login);
// Initialize AccountKit
AccountKit.initialize(getApplicationContext());
// Check if there is already a logged in user
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
if (user == null) {
// no logged in user found. Initate the login flow
onLoginPhone();
} else {
// User exists. Do something else!
}
}
/** Request code for use in onActivityResult **/
public static int ACCOUNT_KIT_LOGIN_REQUEST = 99;
@Override
protected void onActivityResult(
final int requestCode,
final int resultCode,
final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == ACCOUNT_KIT_LOGIN_REQUEST) {
// Pass the request to FacebookAccountKit, with this activity as the listener
new FirebaseAccountKit().handleAccountKitResult(data, SERVER_URL, this);
}
}
/**
* Helper method to initiate the AccountKit SMS-based login flow.
* Make sure to use AccountKitActivity.ResponseType.CODE for the
* response type, and not token. This way, the server can verify
* that the code is valid, generate an access token and create a
* Firebase user.
*/
public void onLoginPhone() {
final Intent intent = new Intent(this, AccountKitActivity.class);
AccountKitConfiguration.AccountKitConfigurationBuilder configurationBuilder =
new AccountKitConfiguration.AccountKitConfigurationBuilder(
LoginType.PHONE,
AccountKitActivity.ResponseType.CODE);
intent.putExtra(
AccountKitActivity.ACCOUNT_KIT_ACTIVITY_CONFIGURATION,
configurationBuilder.build());
startActivityForResult(intent, ACCOUNT_KIT_LOGIN_REQUEST);
}
/** Callback for successful login. Simply prints the authorization code and user ID **/
@Override
public void onFirebaseAccountKitSuccess(
AccountKitLoginResult accountKitLoginResult,
JSONObject authServerResult,
AuthResult firebaseAuthResult) {
String resultStr = "";
resultStr += "authCode: " + accountKitLoginResult.getAuthorizationCode() + "\n";
resultStr += "fireBaseUid: " + firebaseAuthResult.getUser().getUid();
((TextView) findViewById(R.id.result_text)).setText(resultStr);
}
/** Callback for failed login. Use the error code to identify the cause of failure **/
@Override
public void onFirebaseAccountKitFailure(
int errorCode,
AccountKitLoginResult accountKitLoginResult,
String authServerResult,
Exception e) {
((TextView) findViewById(R.id.result_text)).setText(FirebaseAccountKit.getDescription(errorCode));
}
}
// Express (Node JS) server to handle authentication requests
// Firebase Reference: https://firebase.google.com/docs/server/setup
// AccountKit Reference: https://developers.facebook.com/docs/accountkit/graphapi#retrieving-user-access-tokens-with-an-authorization-code
// npm i --save express firebase request
var express = require('express');
var request = require('request');
var firebase = require("firebase");
var app = express();
// Intialize firebase using the JSON credentials file.
firebase.initializeApp({
serviceAccount: "./creds.json",
databaseURL: "https://<YOUR_APP_ID>.firebaseio.com"
});
var FACEBOOK_APP_ID = '<YOUR_FACEBOOK_APP_ID>';
var FACEBOOK_APP_SECRET = '<YOUR_FACEBOOK_APP_SECRET>';
// The authorization code retrieved from AccountKit
// is sent to this endpoint. It retrieves the
// corresponding account details from AccountKit
// Graph API and creates an auth token for a Firebase
// User with the same ID as that in AccountKit
app.get('/create_account', function (req, res) {
var authorizationCode = req.query.authorization_code;
var url = `https://graph.accountkit.com/v1.0/access_token?grant_type=authorization_code&code=${authorizationCode}&access_token=AA|${FACEBOOK_APP_ID}|${FACEBOOK_APP_SECRET}`;
// Query the AccountKit Graph API for account details
request(url, function (error, response, body) {
var json = JSON.parse(body);
// Retrieve the account Id from the response and create a firebase auth token
if (json.id) {
var token = firebase.auth().createCustomToken(json.id);
json.firebase_token = token;
}
// Sent the original Graph API response + firebase token back to the client.
res.send(JSON.stringify(json));
});
});
app.listen(3000, function () {
console.log('Listening on port 3000!')
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment