Skip to content

Instantly share code, notes, and snippets.

@cotyhamilton
Last active August 13, 2024 17:05
Show Gist options
  • Save cotyhamilton/14addb2be92fe7829dcdef0bf5eea10d to your computer and use it in GitHub Desktop.
Save cotyhamilton/14addb2be92fe7829dcdef0bf5eea10d to your computer and use it in GitHub Desktop.
Add Firebase Authentication to Directus ~v9.8.x

Add Firebase Authentication to Directus v9.8.x

Set Up

Install packages

yarn add firebase-admin jsonwebtoken ms nanoid

# or

npm install --save firebase-admin jsonwebtoken ms nanoid

Add or update the following environment variables

AUTH_PROVIDERS="firebase"
AUTH_FIREBASE_DRIVER="local"

GOOGLE_APPLICATION_CREDENTIALS=<path/to/firebaseServiceAccount.json>

Make sure to update the DEFAULT_ROLE_NAME variable in the endpoint with the default role you want users to have on sign up.

Description

Add two extension files, one hook and one endpoint.

extensions/hooks/firebase/index.js
extensions/endpoints/firebase/index.js

The hook initializes firebase on app startup.

The endpoint creates an endpoint on /firebase/connect to accept post requests with firebase id tokens. The endpoint then verifies the id token, looks up the user by email from the decoded token and creates the user if it does not exist in directus. The endpoint then returns an access_token and refresh_token for the user.

Notes

The implemenation is based on this file: https://github.com/directus/directus/blob/e7ada1f173dc6756cce40743023a8873ed2623e2/api/src/services/authentication.ts

Consider error handling and the emitter

At time of writing, the version of directus is 9.8.0

"use strict";
const jwt = require("jsonwebtoken");
const { getAuth } = require("firebase-admin/auth");
const { nanoid } = require("nanoid");
const ms = require("ms");
const DEFAULT_ROLE_NAME = "User"; // update this variable with the name of your role
const PROVIDER_NAME = "firebase";
var endpoint = (router, { services, exceptions, env, database: knex, emitter, logger }) => {
router.post("/connect", async (req, res, next) => {
const { InvalidCredentialsException } = exceptions;
const { accountability, schema } = req;
const idToken = req.body?.id_token;
const { UsersService, RolesService } = services;
const usersService = new UsersService({ schema });
const rolesService = new RolesService({ schema });
let user, role, decodedToken;
// verify token
try {
decodedToken = await getAuth()
.verifyIdToken(idToken);
}
catch (error) {
logger.info(error.message);
return next(new InvalidCredentialsException());
}
// get user
const { email } = decodedToken;
user = await getUser(email, usersService);
// set role if user found, or create new user
if (user?.id) {
role = await getRole(user.role, rolesService);
}
else {
role = await getRole(undefined, rolesService);
const userId = await usersService.createOne({
email,
provider: PROVIDER_NAME,
role: role.id
});
user = await usersService.readOne(userId);
}
const tokenPayload = {
id: user.id,
role: user.role,
app_access: role.app_access,
admin_access: role.admin_access,
};
// sign jwt for user
const accessToken = jwt.sign(tokenPayload, env.SECRET, {
expiresIn: env.ACCESS_TOKEN_TTL,
issuer: "directus",
});
const refreshToken = nanoid(64);
const refreshTokenExpiration = new Date(Date.now() + ms(env.REFRESH_TOKEN_TTL));
await knex('directus_sessions').insert({
token: refreshToken,
user: user.id,
expires: refreshTokenExpiration,
ip: accountability?.ip,
user_agent: accountability?.userAgent,
});
await knex('directus_sessions').delete().where('expires', '<', new Date());
await knex('directus_users').update({ last_access: new Date() }).where({ id: user.id });
res.json({
data: {
accessToken,
refreshToken,
expires: ms(env.ACCESS_TOKEN_TTL),
}
});
});
};
const getUser = async (email, service) => {
const user = (await service.readByQuery({
fields: ["*"],
filter: {
email: {
_eq: email
}
}
}))[0];
return user;
}
const getRole = async (roleId, service) => {
let role;
if (roleId) {
role = await service.readOne(roleId);
}
else {
role = (await service.readByQuery({
fields: ["id", "admin_access", "app_access"],
filter: {
name: {
_eq: DEFAULT_ROLE_NAME
}
}
}))[0];
}
return role;
}
module.exports = endpoint;
"use strict";
const { initializeApp, applicationDefault } = require("firebase-admin/app");
var hook = ({ init: hook }, { services, exceptions, env, database: knex, emitter, logger }) => {
hook("cli.before", (async (program) => {
try {
initializeApp({ credential: applicationDefault() });
logger.info("Firebase initialized");
}
catch (error) {
logger.fatal({ error }, "Firebase failed to initalize");
throw error;
}
}));
};
module.exports = hook;
@navanshu
Copy link

How do you use it on the front end? Does it mean one can use directus sdk?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment