Merge pull request #2797 from akhilmhdh/feat/oauth2-csrf

feat: resolved csrf for oauth2 using state parameter
This commit is contained in:
Akhil Mohan
2024-12-05 14:37:47 +05:30
committed by GitHub
8 changed files with 116 additions and 52 deletions
+4 -1
View File
@@ -12,6 +12,7 @@ import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
import ratelimiter from "@fastify/rate-limit";
import { fastifyRequestContext } from "@fastify/request-context";
import fastify from "fastify";
import { Redis } from "ioredis";
import { Knex } from "knex";
import { HsmModule } from "@app/ee/services/hsm/hsm-types";
@@ -41,10 +42,11 @@ type TMain = {
queue: TQueueServiceFactory;
keyStore: TKeyStoreFactory;
hsmModule: HsmModule;
redis: Redis;
};
// Run the server!
export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore }: TMain) => {
export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore, redis }: TMain) => {
const appCfg = getConfig();
const server = fastify({
@@ -60,6 +62,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
server.setValidatorCompiler(validatorCompiler);
server.setSerializerCompiler(serializerCompiler);
server.decorate("redis", redis);
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try {
const strBody = body instanceof Buffer ? body.toString() : body;
+94 -40
View File
@@ -8,6 +8,7 @@
import { Authenticator } from "@fastify/passport";
import fastifySession from "@fastify/session";
import RedisStore from "connect-redis";
import { Strategy as GitHubStrategy } from "passport-github";
import { Strategy as GitLabStrategy } from "passport-gitlab2";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
@@ -23,8 +24,22 @@ import { OrgAuthMethod } from "@app/services/org/org-types";
export const registerSsoRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
const passport = new Authenticator({ key: "sso", userProperty: "passportUser" });
await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY });
const redisStore = new RedisStore({
client: server.redis,
prefix: "oauth-session:",
ttl: 600 // 10 minutes
});
await server.register(fastifySession, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
store: redisStore,
cookie: {
secure: appCfg.HTTPS_ENABLED,
sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server
}
});
await server.register(passport.initialize());
await server.register(passport.secureSession());
// passport oauth strategy for Google
@@ -37,11 +52,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientID: appCfg.CLIENT_ID_GOOGLE_LOGIN as string,
clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`,
scope: ["profile", " email"]
scope: ["profile", " email"],
state: true
},
// eslint-disable-next-line
async (req, _accessToken, _refreshToken, profile, cb) => {
try {
// @ts-expect-error this is because this is express type and not fastify
const callbackPort = req.session.get("callbackPort");
const email = profile?.emails?.[0]?.value;
if (!email)
throw new NotFoundError({
@@ -54,7 +73,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
firstName: profile?.name?.givenName || "",
lastName: profile?.name?.familyName || "",
authMethod: AuthMethod.GOOGLE,
callbackPort: req.query.state as string
callbackPort
});
cb(null, { isUserCompleted, providerAuthToken });
} catch (error) {
@@ -76,10 +95,14 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string,
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`,
scope: ["user:email"]
scope: ["user:email"],
// akhilmhdh: because the ts type for this is outdated by the maintainer
state: true as unknown as string
},
// eslint-disable-next-line
async (req, accessToken, _refreshToken, profile, cb) => {
// @ts-expect-error this is because this is express type and not fastify
const callbackPort = req.session.get("callbackPort");
try {
const ghEmails = await fetchGithubEmails(accessToken);
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
@@ -88,7 +111,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
firstName: profile.displayName,
lastName: "",
authMethod: AuthMethod.GITHUB,
callbackPort: req.query.state as string
callbackPort
});
return cb(null, { isUserCompleted, providerAuthToken });
} catch (error) {
@@ -112,17 +135,20 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientID: appCfg.CLIENT_ID_GITLAB_LOGIN,
clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`,
baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL
baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL,
state: true
},
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
try {
const callbackPort = req.session.get("callbackPort");
const email = profile.emails[0].value;
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
email,
firstName: profile.displayName,
lastName: "",
authMethod: AuthMethod.GITLAB,
callbackPort: req.query.state as string
callbackPort
});
return cb(null, { isUserCompleted, providerAuthToken });
@@ -143,17 +169,24 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
callback_port: z.string().optional()
})
},
preValidation: (req, res) =>
(
passport.authenticate("google", {
scope: ["profile", "email"],
session: false,
state: req.query.callback_port,
authInfo: false
// this is due to zod type difference
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any
)(req, res),
preValidation: [
async (req, res) => {
const { callback_port: callbackPort } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
if (callbackPort) {
req.session.set("callbackPort", callbackPort);
}
return (
passport.authenticate("google", {
scope: ["profile", "email"],
authInfo: false
// this is due to zod type difference
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any
)(req, res);
}
],
handler: () => {}
});
@@ -166,7 +199,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
authInfo: false
// this is due to zod type difference
}) as never,
handler: (req, res) => {
handler: async (req, res) => {
await req.session.destroy();
if (req.passportUser.isUserCompleted) {
return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
@@ -186,15 +220,24 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
callback_port: z.string().optional()
})
},
preValidation: (req, res) =>
(
passport.authenticate("github", {
session: false,
state: req.query.callback_port,
authInfo: false
// this is due to zod type difference
}) as any
)(req, res),
preValidation: [
async (req, res) => {
const { callback_port: callbackPort } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
if (callbackPort) {
req.session.set("callbackPort", callbackPort);
}
return (
passport.authenticate("github", {
session: false,
authInfo: false
// this is due to zod type difference
}) as any
)(req, res);
}
],
handler: () => {}
});
@@ -245,7 +288,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
authInfo: false
// this is due to zod type difference
}) as any,
handler: (req, res) => {
handler: async (req, res) => {
await req.session.destroy();
if (req.passportUser.isUserCompleted) {
return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
@@ -265,16 +309,25 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
callback_port: z.string().optional()
})
},
preValidation: (req, res) =>
(
passport.authenticate("gitlab", {
session: false,
state: req.query.callback_port,
authInfo: false
// this is due to zod type difference
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any
)(req, res),
preValidation: [
async (req, res) => {
const { callback_port: callbackPort } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
if (callbackPort) {
req.session.set("callbackPort", callbackPort);
}
return (
passport.authenticate("gitlab", {
session: false,
authInfo: false
// this is due to zod type difference
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any
)(req, res);
}
],
handler: () => {}
});
@@ -288,7 +341,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
// this is due to zod type difference
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any,
handler: (req, res) => {
handler: async (req, res) => {
await req.session.destroy();
if (req.passportUser.isUserCompleted) {
return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`