videofolxtv/server/replitAuth.ts
sebastjanartic d91a94675c Improve user ID generation for consistent authentication
Implement deterministic UUID generation using SHA256 hashing for Replit user IDs to ensure consistent user identification across sessions and database operations.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 170e18f0-0f13-4eca-8643-546bba1dd8cc
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/170e18f0-0f13-4eca-8643-546bba1dd8cc/vqbrWR9
2025-09-02 12:47:24 +00:00

192 lines
5.5 KiB
TypeScript

import * as client from "openid-client";
import { Strategy, type VerifyFunction } from "openid-client/passport";
import passport from "passport";
import session from "express-session";
import type { Express, RequestHandler } from "express";
import memoize from "memoizee";
import connectPg from "connect-pg-simple";
import { storage } from "./storage";
import { createHash } from "crypto";
if (!process.env.REPLIT_DOMAINS) {
throw new Error("Environment variable REPLIT_DOMAINS not provided");
}
const getOidcConfig = memoize(
async () => {
return await client.discovery(
new URL(process.env.ISSUER_URL ?? "https://replit.com/oidc"),
process.env.REPL_ID!
);
},
{ maxAge: 3600 * 1000 }
);
export function getSession() {
const sessionTtl = 7 * 24 * 60 * 60 * 1000; // 1 week
const pgStore = connectPg(session);
const sessionStore = new pgStore({
conString: process.env.DATABASE_URL,
createTableIfMissing: false,
ttl: sessionTtl,
tableName: "sessions",
});
return session({
secret: process.env.SESSION_SECRET!,
store: sessionStore,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: true,
maxAge: sessionTtl,
},
});
}
function updateUserSession(
user: any,
tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers
) {
user.claims = tokens.claims();
user.access_token = tokens.access_token;
user.refresh_token = tokens.refresh_token;
user.expires_at = user.claims?.exp;
}
function generateDeterministicUUID(replitId: string): string {
// Create a deterministic UUID from Replit ID
// This ensures the same Replit user always gets the same UUID
const hash = createHash('sha256').update(`replit_${replitId}`).digest('hex');
// Format as UUID v4: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
return [
hash.substring(0, 8),
hash.substring(8, 12),
'4' + hash.substring(13, 16),
(parseInt(hash.substring(16, 17), 16) & 0x3 | 0x8).toString(16) + hash.substring(17, 20),
hash.substring(20, 32)
].join('-');
}
async function upsertUser(claims: any) {
// Generate a deterministic UUID from Replit ID
const userId = generateDeterministicUUID(claims["sub"]);
await storage.upsertUser({
id: userId,
email: claims["email"],
firstName: claims["first_name"],
lastName: claims["last_name"],
profileImageUrl: claims["profile_image_url"],
});
}
export async function setupAuth(app: Express) {
app.set("trust proxy", 1);
app.use(getSession());
app.use(passport.initialize());
app.use(passport.session());
const config = await getOidcConfig();
const verify: VerifyFunction = async (
tokens: client.TokenEndpointResponse & client.TokenEndpointResponseHelpers,
verified: passport.AuthenticateCallback
) => {
const user = {};
updateUserSession(user, tokens);
await upsertUser(tokens.claims());
verified(null, user);
};
for (const domain of process.env.REPLIT_DOMAINS!.split(",")) {
const strategy = new Strategy(
{
name: `replitauth:${domain}`,
config,
scope: "openid email profile offline_access",
callbackURL: `https://${domain}/api/callback`,
},
verify,
);
passport.use(strategy);
}
passport.serializeUser((user: Express.User, cb) => cb(null, user));
passport.deserializeUser((user: Express.User, cb) => cb(null, user));
app.get("/api/login", (req, res, next) => {
passport.authenticate(`replitauth:${req.hostname}`, {
prompt: "login consent",
scope: ["openid", "email", "profile", "offline_access"],
})(req, res, next);
});
app.get("/api/callback", (req, res, next) => {
passport.authenticate(`replitauth:${req.hostname}`, {
successReturnToOrRedirect: "/admin",
failureRedirect: "/api/login",
})(req, res, next);
});
app.get("/api/logout", (req, res) => {
req.logout(() => {
res.redirect(
client.buildEndSessionUrl(config, {
client_id: process.env.REPL_ID!,
post_logout_redirect_uri: `${req.protocol}://${req.hostname}`,
}).href
);
});
});
}
export const isAuthenticated: RequestHandler = async (req, res, next) => {
const user = req.user as any;
if (!req.isAuthenticated() || !user.expires_at) {
return res.status(401).json({ message: "Unauthorized" });
}
const now = Math.floor(Date.now() / 1000);
if (now <= user.expires_at) {
return next();
}
const refreshToken = user.refresh_token;
if (!refreshToken) {
res.status(401).json({ message: "Unauthorized" });
return;
}
try {
const config = await getOidcConfig();
const tokenResponse = await client.refreshTokenGrant(config, refreshToken);
updateUserSession(user, tokenResponse);
return next();
} catch (error) {
res.status(401).json({ message: "Unauthorized" });
return;
}
};
export const isAdmin: RequestHandler = async (req, res, next) => {
const user = req.user as any;
if (!req.isAuthenticated() || !user.claims?.sub) {
return res.status(401).json({ message: "Unauthorized" });
}
try {
// Generate the same deterministic UUID from Replit ID
const userId = generateDeterministicUUID(user.claims.sub);
const dbUser = await storage.getUser(userId);
if (!dbUser || !dbUser.isAdmin) {
return res.status(403).json({ message: "Admin access required" });
}
next();
} catch (error) {
console.error("Error checking admin status:", error);
res.status(500).json({ message: "Internal server error" });
}
};