Skip to main content

@firebase-backup-platform/firebase-oauth

The firebase-oauth package provides OAuth 2.0 integration for connecting Firebase projects. It handles token management, refresh, and secure credential storage.

Installation

npm install @firebase-backup-platform/firebase-oauth
# or
yarn add @firebase-backup-platform/firebase-oauth

Overview

This package enables secure OAuth-based connections to Firebase projects, eliminating the need to manually manage service account keys:


Quick Start

Initialize the OAuth Client

import { FirebaseOAuth } from '@firebase-backup-platform/firebase-oauth';

const oauth = new FirebaseOAuth({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
redirectUri: 'https://your-app.com/oauth/callback'
});

Generate Authorization URL

const authUrl = oauth.getAuthorizationUrl({
scopes: [
'https://www.googleapis.com/auth/firebase',
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/datastore'
],
state: 'unique-session-id',
accessType: 'offline', // Required for refresh tokens
prompt: 'consent' // Force consent to get refresh token
});

// Redirect user to authUrl
console.log('Redirect to:', authUrl);

Handle OAuth Callback

// In your callback route handler
app.get('/oauth/callback', async (req, res) => {
const { code, state } = req.query;

// Verify state matches session
if (state !== sessionState) {
return res.status(400).send('Invalid state');
}

// Exchange code for tokens
const tokens = await oauth.exchangeCodeForTokens(code);

console.log('Access Token:', tokens.accessToken);
console.log('Refresh Token:', tokens.refreshToken);
console.log('Expires At:', tokens.expiresAt);

// Store tokens securely
await storeTokens(userId, tokens);

res.redirect('/dashboard');
});

API Reference

FirebaseOAuth Class

Constructor

new FirebaseOAuth(config: OAuthConfig)

OAuthConfig:

PropertyTypeRequiredDescription
clientIdstringYesGoogle OAuth client ID
clientSecretstringYesGoogle OAuth client secret
redirectUristringYesOAuth callback URL
tokenStoreTokenStoreNoCustom token storage

Methods

getAuthorizationUrl(options)

Generates the OAuth authorization URL.

getAuthorizationUrl(options: AuthUrlOptions): string

AuthUrlOptions:

PropertyTypeDefaultDescription
scopesstring[]RequiredOAuth scopes to request
statestringRandomCSRF protection token
accessType'online' | 'offline''offline'Token type
promptstring'consent'Consent prompt behavior
loginHintstring-Pre-fill email address
includeGrantedScopesbooleantrueInclude existing scopes

Example:

const authUrl = oauth.getAuthorizationUrl({
scopes: [
'https://www.googleapis.com/auth/firebase',
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/datastore'
],
state: crypto.randomBytes(16).toString('hex'),
accessType: 'offline',
prompt: 'consent',
loginHint: 'user@example.com'
});
exchangeCodeForTokens(code)

Exchanges authorization code for tokens.

async exchangeCodeForTokens(code: string): Promise<TokenSet>

TokenSet:

interface TokenSet {
accessToken: string;
refreshToken?: string;
tokenType: string;
expiresIn: number;
expiresAt: Date;
scope: string;
idToken?: string;
}

Example:

const tokens = await oauth.exchangeCodeForTokens(code);

// Store refresh token securely (encrypted)
await db.users.update({
where: { id: userId },
data: {
googleRefreshToken: encrypt(tokens.refreshToken),
tokenExpiresAt: tokens.expiresAt
}
});
refreshAccessToken(refreshToken)

Refreshes an expired access token.

async refreshAccessToken(refreshToken: string): Promise<TokenSet>

Example:

// Check if token needs refresh
if (isTokenExpired(storedToken)) {
const decryptedRefreshToken = decrypt(user.googleRefreshToken);
const newTokens = await oauth.refreshAccessToken(decryptedRefreshToken);

// Update stored token
await db.users.update({
where: { id: userId },
data: {
accessToken: encrypt(newTokens.accessToken),
tokenExpiresAt: newTokens.expiresAt
}
});
}
revokeToken(token)

Revokes an access or refresh token.

async revokeToken(token: string): Promise<void>

Example:

// When user disconnects their account
await oauth.revokeToken(user.refreshToken);

// Clear stored tokens
await db.users.update({
where: { id: userId },
data: {
googleRefreshToken: null,
accessToken: null
}
});
getTokenInfo(accessToken)

Gets information about a token.

async getTokenInfo(accessToken: string): Promise<TokenInfo>

Example:

const info = await oauth.getTokenInfo(accessToken);

console.log('Email:', info.email);
console.log('Scopes:', info.scope);
console.log('Expires in:', info.expiresIn);
console.log('Valid:', info.valid);

Token Management

Automatic Token Refresh

Use the TokenManager for automatic token refresh:

import { TokenManager } from '@firebase-backup-platform/firebase-oauth';

const tokenManager = new TokenManager({
oauth,
onTokenRefresh: async (userId, newTokens) => {
// Called when tokens are refreshed
await db.users.update({
where: { id: userId },
data: {
accessToken: encrypt(newTokens.accessToken),
tokenExpiresAt: newTokens.expiresAt
}
});
},
refreshBuffer: 300 // Refresh 5 minutes before expiry
});

// Get a valid access token (auto-refreshes if needed)
const accessToken = await tokenManager.getValidToken(userId);

Custom Token Storage

Implement custom token storage:

import { TokenStore } from '@firebase-backup-platform/firebase-oauth';

class RedisTokenStore implements TokenStore {
private redis: Redis;

constructor(redis: Redis) {
this.redis = redis;
}

async get(key: string): Promise<TokenSet | null> {
const data = await this.redis.get(`tokens:${key}`);
return data ? JSON.parse(data) : null;
}

async set(key: string, tokens: TokenSet): Promise<void> {
await this.redis.set(
`tokens:${key}`,
JSON.stringify(tokens),
'EX',
tokens.expiresIn
);
}

async delete(key: string): Promise<void> {
await this.redis.del(`tokens:${key}`);
}
}

const oauth = new FirebaseOAuth({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
redirectUri: 'https://your-app.com/oauth/callback',
tokenStore: new RedisTokenStore(redis)
});

Firebase Admin SDK Integration

Creating Admin SDK Instance

import { FirebaseOAuth, createFirebaseAdmin } from '@firebase-backup-platform/firebase-oauth';

// Get valid access token
const accessToken = await tokenManager.getValidToken(userId);

// Create Firebase Admin instance with OAuth credentials
const admin = await createFirebaseAdmin({
projectId: 'my-firebase-project',
accessToken
});

// Use Firestore
const firestore = admin.firestore();
const users = await firestore.collection('users').get();

OAuth Credential Provider

For continuous access with auto-refresh:

import { OAuthCredentialProvider } from '@firebase-backup-platform/firebase-oauth';

const credentialProvider = new OAuthCredentialProvider({
tokenManager,
userId
});

const admin = await createFirebaseAdmin({
projectId: 'my-firebase-project',
credentialProvider
});

// Credentials automatically refresh when needed

Required Scopes

Essential Scopes

ScopePurpose
https://www.googleapis.com/auth/firebaseFirebase access
https://www.googleapis.com/auth/cloud-platformGoogle Cloud access
https://www.googleapis.com/auth/datastoreFirestore access

Optional Scopes

ScopePurpose
emailGet user email
profileGet user profile info
https://www.googleapis.com/auth/firebase.readonlyRead-only Firebase access

Full Scope Configuration

const REQUIRED_SCOPES = [
'email',
'profile',
'https://www.googleapis.com/auth/firebase',
'https://www.googleapis.com/auth/cloud-platform',
'https://www.googleapis.com/auth/datastore'
];

const authUrl = oauth.getAuthorizationUrl({
scopes: REQUIRED_SCOPES,
accessType: 'offline',
prompt: 'consent'
});

Security Best Practices

Token Encryption

Always encrypt tokens at rest:

import crypto from 'crypto';

const ENCRYPTION_KEY = Buffer.from(process.env.TOKEN_ENCRYPTION_KEY, 'hex');

function encryptToken(token: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);

let encrypted = cipher.update(token, 'utf8', 'hex');
encrypted += cipher.final('hex');

const authTag = cipher.getAuthTag();

return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}

function decryptToken(encryptedToken: string): string {
const [ivHex, authTagHex, encrypted] = encryptedToken.split(':');

const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');

const decipher = crypto.createDecipheriv('aes-256-gcm', ENCRYPTION_KEY, iv);
decipher.setAuthTag(authTag);

let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');

return decrypted;
}

State Parameter Validation

Always validate the state parameter:

import { v4 as uuidv4 } from 'uuid';

// Generate state and store in session
const state = uuidv4();
req.session.oauthState = state;

const authUrl = oauth.getAuthorizationUrl({
scopes: REQUIRED_SCOPES,
state
});

// In callback
app.get('/oauth/callback', (req, res) => {
if (req.query.state !== req.session.oauthState) {
return res.status(400).send('Invalid state parameter');
}

// Process callback...
});

Token Revocation on Disconnect

async function disconnectProject(userId: string, projectId: string) {
// Get user's tokens
const user = await db.users.findUnique({ where: { id: userId } });

// Revoke tokens
if (user.refreshToken) {
try {
await oauth.revokeToken(decrypt(user.refreshToken));
} catch (error) {
console.warn('Token revocation failed:', error);
}
}

// Clear stored tokens
await db.users.update({
where: { id: userId },
data: {
refreshToken: null,
accessToken: null,
tokenExpiresAt: null
}
});

// Remove project connection
await db.projectConnections.delete({
where: { userId_projectId: { userId, projectId } }
});
}

Error Handling

import {
FirebaseOAuth,
OAuthError,
TokenExpiredError,
InvalidGrantError,
InsufficientScopesError
} from '@firebase-backup-platform/firebase-oauth';

try {
const tokens = await oauth.exchangeCodeForTokens(code);
} catch (error) {
if (error instanceof TokenExpiredError) {
// Token has expired, refresh it
const newTokens = await oauth.refreshAccessToken(refreshToken);
} else if (error instanceof InvalidGrantError) {
// User revoked access or token is invalid
// Prompt user to reconnect
redirectToOAuth();
} else if (error instanceof InsufficientScopesError) {
// Missing required scopes
// Re-request with all scopes
const authUrl = oauth.getAuthorizationUrl({
scopes: REQUIRED_SCOPES,
prompt: 'consent'
});
} else if (error instanceof OAuthError) {
console.error('OAuth error:', error.message);
console.error('Error code:', error.code);
}
}

Express Integration

import express from 'express';
import { FirebaseOAuth } from '@firebase-backup-platform/firebase-oauth';

const app = express();
const oauth = new FirebaseOAuth(config);

// Initiate OAuth flow
app.get('/auth/google', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;

const authUrl = oauth.getAuthorizationUrl({
scopes: REQUIRED_SCOPES,
state,
accessType: 'offline',
prompt: 'consent'
});

res.redirect(authUrl);
});

// Handle OAuth callback
app.get('/auth/google/callback', async (req, res) => {
try {
const { code, state, error } = req.query;

if (error) {
return res.redirect('/error?message=' + error);
}

if (state !== req.session.oauthState) {
return res.status(400).send('Invalid state');
}

const tokens = await oauth.exchangeCodeForTokens(code);

// Store tokens and create session
await storeTokens(req.session.userId, tokens);

res.redirect('/dashboard');
} catch (error) {
console.error('OAuth callback error:', error);
res.redirect('/error');
}
});

// Disconnect account
app.post('/auth/disconnect', async (req, res) => {
await disconnectProject(req.session.userId, req.body.projectId);
res.json({ success: true });
});

TypeScript Support

import {
FirebaseOAuth,
OAuthConfig,
AuthUrlOptions,
TokenSet,
TokenInfo,
TokenStore,
TokenManager,
TokenManagerConfig,
OAuthError
} from '@firebase-backup-platform/firebase-oauth';

// Fully typed configuration
const config: OAuthConfig = {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: 'https://your-app.com/oauth/callback'
};

const oauth = new FirebaseOAuth(config);

// Typed options
const authOptions: AuthUrlOptions = {
scopes: ['https://www.googleapis.com/auth/firebase'],
accessType: 'offline'
};

// Typed results
const tokens: TokenSet = await oauth.exchangeCodeForTokens(code);
const info: TokenInfo = await oauth.getTokenInfo(tokens.accessToken);