2

I have a web app built with Gatsby that has client-side authentication through Auth0. I want to use Firebase as a database for my project, but I need to authenticate users first before they can read/write to Firebase.

The Firebase SDK (firebase-admin) has a function called signInWithCustomToken(token) that I thought I could pass the token from Auth0 into, but this doesn't work (see: https://community.auth0.com/t/react-auth0-firebase/11392).

Instead, I need to proxy Auth0's token through an API which will use firebase-admin to issue a token. Because my Gatsby site is hosted on Netlify, I'm planning to use Netlify Lambda Functions to get proxy Auth0's token. This is where I'm getting stuck.

I've followed this tutorial on how to use Netlify Lambda Functions with Gastsby: https://www.gatsbyjs.org/blog/2018-12-17-turning-the-static-dynamic/

I then went into my Auth.js file where my Auth0 code is and dropped a fetch call in the setSession. I passed the idToken from Auth0 into the url in the fetch function. I'm not sure if this is the right thing to do. I've read in the tutorial that it would be passed in an authorization header, but I'm unclear what that means. Anyways, here's the complete auth.js file:

import auth0 from 'auth0-js';

const windowGlobal = typeof window !== 'undefined' && window;

class Auth {
  auth0 = new auth0.WebAuth({
    domain: process.env.Auth_Domain,
    clientID: process.env.Auth_ClientId,
    redirectUri: process.env.Auth_Callback,
    responseType: 'token id_token',
    scope: 'openid profile email',
  });

  constructor() {
    this.login = this.login.bind(this);
    this.logout = this.logout.bind(this);
    this.handleAuthentication = this.handleAuthentication.bind(this);
    this.isAuthenticated = this.isAuthenticated.bind(this);
  }

  login() {
    this.auth0.authorize();
  }

  logout() {

    // Remove the locally cached profile to avoid confusing errors.
    localStorage.removeItem('access_token');
    localStorage.removeItem('id_token');
    localStorage.removeItem('expires_at');
    localStorage.removeItem('user');

    windowGlobal.window.location.replace(`https://login.skillthrive.com/v2/logout/?returnTo=http%3A%2F%2Flocalhost:8000`)

  }

  handleAuthentication() {
    if (typeof window !== 'undefined') {
      this.auth0.parseHash((err, authResult) => {
        if (authResult && authResult.accessToken && authResult.idToken) {
          this.setSession(authResult)
        } else if (err) {
          console.log(err);
        }
      });
    }
  }

  isAuthenticated() {
    const expiresAt = JSON.parse(localStorage.getItem('expires_at'));
    return new Date().getTime() < expiresAt;
  }

  setSession(authResult) {
    const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + new Date().getTime());
    localStorage.setItem('access_token', authResult.accessToken);
    localStorage.setItem('id_token', authResult.idToken);
    localStorage.setItem('expires_at', expiresAt);

    fetch(`/.netlify/functions/firebase?id=${authResult.idToken}`)
    .then(response => console.log(response))

    this.auth0.client.userInfo(authResult.accessToken, (err, user) => {
      localStorage.setItem('user', JSON.stringify(user));
    })
  }

  getUser() {
    if (localStorage.getItem('user')) {
      return JSON.parse(localStorage.getItem('user'));
    }
  }

  getUserName() {
    if (this.getUser()) {
      return this.getUser().name;
    }
  }

}

export default Auth;

I found a tutorial called How to Authenticate Firebase and Angular with Auth0 that has a function that mints a token for Firebase:

const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const firebaseAdmin = require('firebase-admin');
// Config
const config = require('./config');

module.exports = function(app) {
  // Auth0 athentication middleware
  const jwtCheck = jwt({
    secret: jwks.expressJwtSecret({
      cache: true,
      rateLimit: true,
      jwksRequestsPerMinute: 5,
      jwksUri: `https://${config.AUTH0_DOMAIN}/.well-known/jwks.json`
    }),
    audience: config.AUTH0_API_AUDIENCE,
    issuer: `https://${config.AUTH0_DOMAIN}/`,
    algorithm: 'RS256'
  });

  // Initialize Firebase Admin with service account
  const serviceAccount = require(config.FIREBASE_KEY);
  firebaseAdmin.initializeApp({
    credential: firebaseAdmin.credential.cert(serviceAccount),
    databaseURL: config.FIREBASE_DB
  });

app.get('/auth/firebase', jwtCheck, (req, res) => {
    // Create UID from authenticated Auth0 user
    const uid = req.user.sub;
    // Mint token using Firebase Admin SDK
    firebaseAdmin.auth().createCustomToken(uid)
      .then(customToken => 
        // Response must be an object or Firebase errors
        res.json({firebaseToken: customToken})
      )
      .catch(err => 
        res.status(500).send({
          message: 'Something went wrong acquiring a Firebase token.',
          error: err
        })
      );
  });

I tried to incorporate small parts at a time into my Lambda function:

var admin = require("firebase-admin");
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');

// For more info, check https://www.netlify.com/docs/functions/#javascript-lambda-functions
export function handler(event, context, callback) {
  console.log("queryStringParameters", event.queryStringParameters);

  const jwtCheck = jwt({
    secret: jwks.expressJwtSecret({
      cache: true,
      rateLimit: true,
      jwksRequestsPerMinute: 5,
      jwksUri: `https://${process.env.Auth_Domain}/.well-known/jwks.json`
    }),
    audience: process.env.Auth_Audience,
    issuer: `https://${process.env.Auth_Domain}/`,
    algorithm: 'RS256'
  });

  callback(null, {
    // return null to show no errors
    statusCode: 200, // http status code
    body: JSON.stringify({
      msg: "Hello, World! " + Math.round(Math.random() * 10),
    }),
  })
}

I tried checking to see what came back for jwtCheck by console logging it, but all I got was something weird { [Function: d] unless: [Function], UnauthorizedError: [Function: r] }

How should I go about incorporating this into my Lambda function?

1 Answer 1

2

I found a module called serverless-http that allows me to write Lambda Function as if it were written in Express. This made it easy for me to wrap my head around what was happening, so I finally got this code to return the new minted token from Firebase:

const express = require('express');
const serverless = require('serverless-http');
const cors = require('cors');
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const firebaseAdmin = require('firebase-admin');

const app = express();
app.use(cors());

const jwtCheck = jwt({
  secret: jwks.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: `${process.env.Auth_Domain}/.well-known/jwks.json`
  }),
  audience: `${process.env.Auth_ClientId}`,
  issuer: `${process.env.Auth_Domain}`,
  algorithm: 'RS256'
});

const serviceAccount = require('../firebase/firebase-keys.json');

firebaseAdmin.initializeApp({
  credential: firebaseAdmin.credential.cert(serviceAccount),
  databaseURL: `https://${serviceAccount.project_id}.firebaseio.com`
});

  // GET object containing Firebase custom token
  app.get('/firebase', jwtCheck, async (req, res) => {
    const {sub: uid} = req.user;

    try {
      const firebaseToken = await firebaseAdmin.auth().createCustomToken(uid);
      res.json({firebaseToken});
    } catch (err) {
      res.status(500).send({
        message: 'Something went wrong acquiring a Firebase token.',
        error: err
      });
    }
  });

module.exports.handler = serverless(app);

Then on the client side I wrapped the fetch call into a function like this and used it when needed:

  async setFirebaseCustomToken() {
    const response = await fetch('/.netlify/functions/firebase', {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('id_token')}`,
      },
    });

    const data = await response.json();
    console.log(data.firebaseToken);
  }

This code is just going to console.log the new token, but now you'll have the response to do what you want with in Firebase client-side. Hope this helps!

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.