@mvp-factory/holy-auth-firebase
Version:
Firebase Authentication module with Google Sign-In support
202 lines (201 loc) • 7.11 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.TokenVerifier = void 0;
exports.createAuthMiddleware = createAuthMiddleware;
const jwt = __importStar(require("jsonwebtoken"));
class TokenVerifier {
constructor(projectId) {
this.publicKeysUrl = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com';
this.keyCache = null;
this.cacheExpiry = 86400000;
if (!projectId) {
throw new Error('Firebase project ID is required');
}
this.projectId = projectId;
}
async verifyIdToken(idToken) {
if (!idToken) {
throw new Error('ID token is required');
}
try {
const publicKeys = await this.getPublicKeys();
const decoded = jwt.decode(idToken, { complete: true });
if (!decoded || typeof decoded === 'string') {
throw new Error('Invalid token format');
}
const { header, payload } = decoded;
const kid = header.kid;
if (!kid || !publicKeys[kid]) {
throw new Error('Invalid key ID');
}
const verified = jwt.verify(idToken, publicKeys[kid], {
algorithms: ['RS256'],
issuer: `https://securetoken.google.com/${this.projectId}`,
audience: this.projectId
});
this.validateClaims(verified);
return {
uid: verified.sub,
email: verified.email || null,
email_verified: verified.email_verified || false,
name: verified.name || null,
picture: verified.picture || null,
auth_time: verified.auth_time,
iat: verified.iat,
exp: verified.exp,
iss: verified.iss,
aud: verified.aud,
sub: verified.sub
};
}
catch (error) {
console.error('Token verification failed:', error);
return null;
}
}
async getPublicKeys() {
if (this.keyCache && this.keyCache.expires > Date.now()) {
return this.keyCache.keys;
}
try {
const response = await fetch(this.publicKeysUrl);
if (!response.ok) {
throw new Error(`Failed to fetch public keys: ${response.statusText}`);
}
const keys = await response.json();
this.keyCache = {
keys,
expires: Date.now() + this.cacheExpiry
};
return keys;
}
catch (error) {
throw new Error(`Failed to fetch public keys: ${error}`);
}
}
validateClaims(claims) {
const now = Math.floor(Date.now() / 1000);
if (!claims.exp || claims.exp < now) {
throw new Error('Token has expired');
}
if (!claims.iat || claims.iat > now) {
throw new Error('Token issued in the future');
}
if (claims.auth_time && claims.auth_time > now) {
throw new Error('Invalid auth time');
}
if (!claims.sub || typeof claims.sub !== 'string') {
throw new Error('Invalid subject');
}
const expectedIssuer = `https://securetoken.google.com/${this.projectId}`;
if (claims.iss !== expectedIssuer) {
throw new Error('Invalid issuer');
}
if (claims.aud !== this.projectId) {
throw new Error('Invalid audience');
}
}
decodeToken(idToken) {
try {
return jwt.decode(idToken);
}
catch (error) {
return null;
}
}
isTokenExpired(idToken) {
try {
const decoded = this.decodeToken(idToken);
if (!decoded || !decoded.exp) {
return true;
}
const now = Math.floor(Date.now() / 1000);
return decoded.exp < now;
}
catch {
return true;
}
}
getTokenExpiration(idToken) {
try {
const decoded = this.decodeToken(idToken);
if (!decoded || !decoded.exp) {
return null;
}
return new Date(decoded.exp * 1000);
}
catch {
return null;
}
}
static async verify(idToken, projectId) {
const verifier = new TokenVerifier(projectId);
return verifier.verifyIdToken(idToken);
}
}
exports.TokenVerifier = TokenVerifier;
function createAuthMiddleware(projectId) {
const verifier = new TokenVerifier(projectId);
return async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Authorization header missing or invalid'
});
}
const idToken = authHeader.substring(7);
const decodedToken = await verifier.verifyIdToken(idToken);
if (!decodedToken) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid token'
});
}
req.user = decodedToken;
req.uid = decodedToken.uid;
next();
}
catch (error) {
console.error('Auth middleware error:', error);
res.status(401).json({
error: 'Unauthorized',
message: 'Authentication failed'
});
}
};
}