Implementing Expo with Firebase Authentication

@pixabay

Who is this article targeted for?

  • have experiece to login developer console on Facebook, Google, Github and Twitter.
  • have experiece implementing with Firebase Authentication.
  • have experiece implementinng with Exp.

Server

mkdir server && cd $_
yarn init
yarn add express body-parser node-fetch oauth-1.0a qa crypto-js
touch server/auth-server.js

create server/auth-server.js

const express = require('express')
const bodyParser = require('body-parser');
const fetch = require('node-fetch');

const OAuth = require('oauth-1.0a');
const qs = require('qs')
const HmacSHA1 = require('crypto-js/hmac-sha1')
const Base64 = require('crypto-js/enc-base64')

const port = process.env.PORT || 3000

const config = {
  GOOGLE: {
    CLIENT_ID: "",
    CLIENT_SECRET: "",
  },
  GITHUB: {
    CLIENT_ID: "",
    CLIENT_SECRET: "",
  },
  TWITTER: {
    CLIENT_ID: "",
    CLIENT_SECRET: "",
  }
}

const app = express();

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.post('/auth/google', async (req, res) => {

  async function createTokenWithGoogleCode(code, redirect_uri) {

    const url = `https://www.googleapis.com/oauth2/v4/token`
    const res = await fetch(url, {
      method: 'POST',
      body: JSON.stringify({
        code,
        client_id: config.GOOGLE.CLIENT_ID,
        client_secret: config.GOOGLE.CLIENT_SECRET,
        redirect_uri,
        grant_type: 'authorization_code'
      })
    });

    return await res.json()
  }

  return res.json(await createTokenWithGoogleCode(req.body.code, req.body.redirect_uri))
});

app.post('/auth/github', async (req, res) => {

  async function createTokenWithGithubCode(code) {
    const url =
      `https://github.com/login/oauth/access_token` +
      `?client_id=${config.GITHUB.CLIENT_ID}` +
      `&client_secret=${config.GITHUB.CLIENT_SECRET}` +
      `&code=${code}`;

    const res = await fetch(url, {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
      },
    });
    return await res.json()
  }

  return res.json(await createTokenWithGithubCode(req.body.code))
});

app.post('/auth/twitter/request_token', async (req, res) => {

  const { redirect_uri } = req.body

  const oauth = OAuth({
    consumer: {
      key: config.TWITTER.CLIENT_ID,
      secret: config.TWITTER.CLIENT_SECRET,
    },
    signature_method: 'HMAC-SHA1',
    hash_function: (baseString, key) => Base64.stringify(HmacSHA1(baseString, key))
  })

  const request_data = {
    url: 'https://api.twitter.com/oauth/request_token',
    method: 'POST',
    data: {
      oauth_callback: redirect_uri,
    }
  };

  const response = await fetch(request_data.url, {
    method: request_data.method,
    headers: oauth.toHeader(oauth.authorize(request_data))
  })

  const text = await response.text();
  return res.json(qs.parse(text))
});

app.post('/auth/twitter/access_token', async (req, res) => {

  const { oauth_token, oauth_token_secret, oauth_verifier } = req.body

  const oauth = OAuth({
    consumer: {
      key: config.TWITTER.CLIENT_ID,
      secret: config.TWITTER.CLIENT_SECRET,
    },
    signature_method: 'HMAC-SHA1',
    hash_function: (baseString, key) => Base64.stringify(HmacSHA1(baseString, key))
  })

  const request_data = {
    url: 'https://api.twitter.com/oauth/access_token',
    method: 'POST',
    data: {
      oauth_verifier,
    },
  }

  const headers = oauth.toHeader(oauth.authorize(request_data, {key: oauth_token, secret: oauth_token_secret}))

  const response = await fetch(request_data.url, {
    method: request_data.method,
    data: request_data.data,
    headers
  })

  if (response.status !== 200) {
    res.status = response.status
    return res.json({message: "something wrong"})
  }
  const text = await response.text();
  return res.json(qs.parse(text))
})

if (!module.parent) {
  app.listen(3000, () => {
    console.log('Example app listening on port 3000!');
  });
}

module.exports = app;

Expo

expo init expo && cd $_
yarn add firebase

edit expo/App.js


import React from 'react'
import { StyleSheet, Text, Button, View } from 'react-native'
import { AuthSession, Facebook } from 'expo';
import firebase from 'firebase';

const AUTH_BASE_URL = "http://localhost:3000"
const REDIRECT_URL = AuthSession.getRedirectUrl();
const FACEBOOK_APP_ID = ""
const GOOGLE_CLIENT_ID = ""
const GITHUB_CLIENT_ID = ""

if (!firebase.apps.length) {
  firebase.initializeApp({
    apiKey: "",
    authDomain: "",
    databaseURL: "",
    projectId: "",
    storageBucket: "",
    messagingSenderId: ""
  })
}

async function createTokenWithCode(provider, code) {
  const url = `${AUTH_BASE_URL}/auth/${provider}/`
  console.log(url)
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      code,
      redirect_uri: REDIRECT_URL // Google で必要
    })
  })
  const json = await res.json();
  console.log(json)
  return json
}

async function getTwitterRequestToken() {
  const url = `${AUTH_BASE_URL}/auth/twitter/request_token`
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      redirect_uri: REDIRECT_URL
    })
  });
  return await res.json();
}

async function getTwitterAccessToken(params) {
  const { oauth_token, oauth_token_secret, oauth_verifier } = params
  const url = `${AUTH_BASE_URL}/auth/twitter/access_token`
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ oauth_token, oauth_token_secret, oauth_verifier })
  });
  return await res.json();
}

export default class App extends React.Component {

  constructor(props) {
    super(props)
    this.state = {
      user: null
    }
    firebase.auth().onAuthStateChanged((user) => {
      if (user) {
        this.setState({ user: user.toJSON() })
        console.log(user)
      } else {
        this.setState({ user: null })
      }
    })
  }

  handleLogout() {
    firebase.auth().signOut()
  }

  async handleFacebookLogin() {

    const { type, token } = await Facebook.logInWithReadPermissionsAsync(
      FACEBOOK_APP_ID,
      { permissions: ['public_profile'] }
    )

    if (type === 'success') {
      const credential = firebase.auth.FacebookAuthProvider.credential(token)
      firebase.auth().signInAndRetrieveDataWithCredential(credential)
    }
  }

  async handleGoogleLogin() {
    const result = await AuthSession.startAsync({
      authUrl:
        `https://accounts.google.com/o/oauth2/v2/auth?` +
        `&client_id=${GOOGLE_CLIENT_ID}` +
        `&redirect_uri=${REDIRECT_URL}` +
        `&response_type=code` +
        `&access_type=offline` +
        `&scope=profile`,
    });

    const { id_token } = await createTokenWithCode('google', result.params.code)

    var credential = firebase.auth.GoogleAuthProvider.credential(id_token);
    firebase.auth().signInAndRetrieveDataWithCredential(credential);
  }

  async handleGithubLogin() {

    const { params } = await AuthSession.startAsync({
      authUrl:
        `https://github.com/login/oauth/authorize` +
        `?client_id=${GITHUB_CLIENT_ID}` +
        `&redirect_uri=${encodeURIComponent(REDIRECT_URL)}` +
        `&scope=user`
    });

    const { access_token } = await createTokenWithCode('github', params.code);

    const credential = firebase.auth.GithubAuthProvider.credential(access_token);
    firebase.auth().signInAndRetrieveDataWithCredential(credential);
  }

  async handleTwitterLogin() {
    const { oauth_token, oauth_token_secret } = await getTwitterRequestToken()

    const { params }  = await AuthSession.startAsync({
      authUrl: `https://api.twitter.com/oauth/authenticate?oauth_token=${oauth_token}`
    });

    const oauth_verifier = params.oauth_verifier
    const result = await getTwitterAccessToken({oauth_token, oauth_token_secret, oauth_verifier})

    const credential = firebase.auth.TwitterAuthProvider.credential(
      result.oauth_token,
      result.oauth_token_secret
    )
    firebase.auth().signInAndRetrieveDataWithCredential(credential);
  }

  render() {
    return (
      <View style={styles.container}>
        <Text>Firebase Authentication Exampe</Text>
        <Button onPress={this.handleFacebookLogin} title="Facebook" />
        <Button onPress={this.handleGoogleLogin} title="Google" />
        <Button onPress={this.handleGithubLogin} title="Github" />
        <Button onPress={this.handleTwitterLogin} title="Twitter" />
        <Button onPress={this.handleLogout} title="Logout" />
        <Text>displayName: {this.state.user && this.state.user.displayName}</Text>
        <Text>providerId: {this.state.user && this.state.user.providerData[0].providerId}</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

Details

Facebook

Expo provide Facebook.logInWithReadPermissionsAsync function. It make us implement authentication very easy.

Google and Github

The functions handleGoogleLogin and handleGithubLogin are almost the same.

  • React Native: "push login button, redirected to autentication page of the provider."
  • User: "OK, I input my ID and Password to the authentication page."
  • Provider: "This is you Authorization Code. You can visit to redirect_uri with it."
  • AuthSession.getRedirectUrl「Ah, the app is still not publish, so redirect_uri is https://auth.expo.io/@your-username/your-app-slug"
  • React Native: "Hey, you again with your Authorization Code. Let's exchange it to 'Access Token'. It required to use Secret key. So I will request server-sidt that."
  • Node.js: "Alright, here is your 'Access Token'."
  • React Native: "OK, We can leave the rest to firebase."
  • Firebase: "GOTCHA"

AuthSession.getRedirectUrl return the schema like yourapp:// when the app is publised.

I implemented Browser-Based Authentication. AuthSession.startAsync is very usefull funnction. It use system browser and share the cookies. So you can avoid to login .requently

AuthSession.getRedirectUrl() is design based on that developers work with serveral network. But iOS stopped distribute app under development. So there may not be much benefits.

Twitter

Twitter gives access token by auth1.0 method. So It is complicated than Google and Github.

OAuth1.0 required complicated signature so I recommend you implent with libraries.

important point

A configuration file expo/app.json has expo.slag propety, it is relatedAuthSession.getRedirectUrl. It required as valid callback, I recommend you decide proper naming first.

recap

React Native required complicated implementing with Firebase Authentication more than I though. My opinion, if you can handle the requirement, you should choose just only Facebook log in. If not so, I hope this article can help you:)