Authenticated firebase functions with custom OpenId Connect provider

Viewed 118

I have implemented a custom oidc authentication provider with firebase. (Which was very easy!)

For reference, the oidc provider I implemented is for Xero (accounting app)

I want to implement authenticated httpsCallable functions that use the accessToken that is returned from the callback but I can't seem to access it in the firebase function.

Ultimately the getTokenSetFromDatabase function from this example is what I need to recreate somehow in firebase function: https://github.com/XeroAPI/xero-node#accounting-api

The context.auth info in the firebase functions contains some authentication data but not any jwts or tokens.


export const getTenants = functions.https.onCall(async (data, context) => {
  await xero.initialize()

  // Can I get the token set somehow from the context
  // Or do I need to save a users token in the firebase database when they login from the front end?
  const tokenSet = getTokenSetFromDatabase(context.auth?.uid)

  await xero.setTokenSet(tokenSet)

  if (tokenSet.expired()) {
    const validTokenSet = await xero.refreshToken()
    // save the new tokenset
  }

  await xero.updateTenants()

  const activeTenantId = xero.tenants[0].tenantId

  return activeTenantId
})

The console log of context.auth.token is:

{
  "name": "Jialx",
  "iss": "https://securetoken.google.com/firebase-app-name",
  "aud": "firebase-app-name",
  "auth_time": 1658994364,
  "user_id": "0000000000000000000000",
  "sub": "0000000000000000000000",
  "iat": 1659007170,
  "exp": 1659010770,
  "email": "example@email.com",
  "email_verified": false,
  "firebase": {
    "identities": { "oidc.xero": [], "email": [] },
    "sign_in_provider": "oidc.xero",
    "sign_in_attributes": {
      "at_hash": "xx-xxxx-xxxx",
      "preferred_username": "example@gmail.com",
      "sid": "000000000000000000000000000",
      "global_session_id": "000000000000000000000000000",
      "xero_userid": "000000000000000000000000000"
    }
  },
  "uid": "0000000000000000000000"
}

Discovery

So i've stumbled across the blocking functions feature when beforeSignIn function can access these oAuth credentials; so I figure that this would be a great place to save them to the DB and retrieve them later (what its built for).

However this doesn't seem to work with my custom OIDC auth provider config:

It does work but its buggy (See answer for details)

1 Answers

Okay so bit of strange behaviour in implementing the blocking function, It either takes a bit of time to apply that change, or its critically important to select the function AND THEN click the checkbox's AND THEN click save...

Either way, I've come back after lunch and now can see my refresh, id, and access tokens inside of the beforeSignIn context parameter.

Further down the Rabbit hole

I deployed a new version of my beforeSignIn function and found that my Additional provider token credentials options had been automatically toggled off...


SUMMARY

For completeness, this is how i've implemented OIDC Authenticated firebase functions that enable me to make authenticated calls and use my providers services (In this case a users accounting data).

  1. Setup OpenID Connect following this guide:

  2. Setup Firebase Functions following this guide

  3. Build out your firebase functions with the requirements you have, This should give you a good start:

import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
import { TokenSetParameters, XeroClient } from 'xero-node'

admin.initializeApp()

const scope = 'offline_access openid profile email accounting.transactions'

const xero = new XeroClient({
  clientId: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  clientSecret: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  redirectUris: ['https://somefirebaseapp.firebaseapp.com/__/auth/handler'],
  scopes: scope.split(' '),
  httpTimeout: 3000,
})

const getTokenSetFromDatabase = (uid: string) =>
  new Promise<TokenSetParameters>(async (res, rej) => {
    await admin
      .database()
      .ref('user/' + uid + '/tokenSet')
      .once('value', (snapshot) => {
        const val = snapshot.val()
        res(val)
      })
  })

export const getTenants = functions.https.onCall(async (data, context) => {
  await xero.initialize()

  const uid = context.auth?.uid

  if (!uid) return null

  const tokenSet = await getTokenSetFromDatabase(uid)
  xero.setTokenSet(tokenSet)

  try {
    const newTokenSet = await xero.refreshToken()
    await admin
      .database()
      .ref('user/' + uid + '/tokenSet')
      .set(newTokenSet)
  } catch (error) {
    console.log(error)
  }

  await xero.updateTenants()

  return xero.tenants
})

export const beforeSignIn = functions.auth.user().beforeSignIn(async (user, context) => {
  if (!context.credential) return

  const { accessToken, expirationTime, idToken, refreshToken } = context.credential

  if (!accessToken || !expirationTime || !idToken || !refreshToken) return

  const tokenSet: TokenSetParameters = {
    access_token: accessToken,
    expires_at: new Date(expirationTime).valueOf(),
    id_token: idToken,
    refresh_token: refreshToken,
    scope,
  }

  await admin
    .database()
    .ref('user/' + user.uid + '/tokenSet')
    .set(tokenSet)
})

  1. Deploy your firebase functions and link your beforeSignIn function to the authentication > settings > blocking functions.

enter image description here

Keep in mind that if you deploy your function again you will need to reenable this. (Or it might be something that gets fixed )

You can actually enable this in the code...

export const beforeCreate = functions.auth
  .user({ blockingOptions: { accessToken: true, idToken: true, refreshToken: true } })
  .beforeCreate(async (user, conte....

Hopefully this helps the next person.. My codes not bulletproof of course, missing all the error handling and logging etc but you get what you pay for.

Related