@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:
| Property | Type | Required | Description |
|---|---|---|---|
clientId | string | Yes | Google OAuth client ID |
clientSecret | string | Yes | Google OAuth client secret |
redirectUri | string | Yes | OAuth callback URL |
tokenStore | TokenStore | No | Custom token storage |
Methods
getAuthorizationUrl(options)
Generates the OAuth authorization URL.
getAuthorizationUrl(options: AuthUrlOptions): string
AuthUrlOptions:
| Property | Type | Default | Description |
|---|---|---|---|
scopes | string[] | Required | OAuth scopes to request |
state | string | Random | CSRF protection token |
accessType | 'online' | 'offline' | 'offline' | Token type |
prompt | string | 'consent' | Consent prompt behavior |
loginHint | string | - | Pre-fill email address |
includeGrantedScopes | boolean | true | Include 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
| Scope | Purpose |
|---|---|
https://www.googleapis.com/auth/firebase | Firebase access |
https://www.googleapis.com/auth/cloud-platform | Google Cloud access |
https://www.googleapis.com/auth/datastore | Firestore access |
Optional Scopes
| Scope | Purpose |
|---|---|
email | Get user email |
profile | Get user profile info |
https://www.googleapis.com/auth/firebase.readonly | Read-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);
Related Packages
- backup-core - Core backup functionality
- encryption - Token encryption utilities