Last active
November 4, 2016 01:06
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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