PKCE for Github, Gitlab, Google, and OIDC SSO

This commit is contained in:
x
2025-04-25 18:45:23 -04:00
parent cf1f83aaa3
commit 68e21ba8ce
6 changed files with 62 additions and 53 deletions
+10 -31
View File
@@ -91,10 +91,10 @@
"ora": "^7.0.1", "ora": "^7.0.1",
"oracledb": "^6.4.0", "oracledb": "^6.4.0",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0", "passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1", "passport-ldapauth": "^3.0.1",
"passport-oauth2": "^1.8.0",
"pg": "^8.11.3", "pg": "^8.11.3",
"pg-boss": "^10.1.5", "pg-boss": "^10.1.5",
"pg-query-stream": "^4.5.3", "pg-query-stream": "^4.5.3",
@@ -135,7 +135,6 @@
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.17.30", "@types/node": "^20.17.30",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9", "@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3", "@types/picomatch": "^2.3.3",
@@ -9871,17 +9870,6 @@
"@types/express": "*" "@types/express": "*"
} }
}, },
"node_modules/@types/passport-github": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@types/passport-github/-/passport-github-1.1.12.tgz",
"integrity": "sha512-VJpMEIH+cOoXB694QgcxuvWy2wPd1Oq3gqrg2Y9DMVBYs9TmH9L14qnqPDZsNMZKBDH+SvqRsGZj9SgHYeDgcA==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-oauth2": "*"
}
},
"node_modules/@types/passport-google-oauth20": { "node_modules/@types/passport-google-oauth20": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.14.tgz",
@@ -18142,9 +18130,10 @@
"integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA==" "integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA=="
}, },
"node_modules/oauth": { "node_modules/oauth": {
"version": "0.9.15", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
"license": "MIT"
}, },
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -18827,17 +18816,6 @@
"url": "https://github.com/sponsors/jaredhanson" "url": "https://github.com/sponsors/jaredhanson"
} }
}, },
"node_modules/passport-github": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz",
"integrity": "sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==",
"dependencies": {
"passport-oauth2": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-gitlab2": { "node_modules/passport-gitlab2": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
@@ -18873,12 +18851,13 @@
} }
}, },
"node_modules/passport-oauth2": { "node_modules/passport-oauth2": {
"version": "1.7.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
"integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
"license": "MIT",
"dependencies": { "dependencies": {
"base64url": "3.x.x", "base64url": "3.x.x",
"oauth": "0.9.x", "oauth": "0.10.x",
"passport-strategy": "1.x.x", "passport-strategy": "1.x.x",
"uid2": "0.0.x", "uid2": "0.0.x",
"utils-merge": "1.x.x" "utils-merge": "1.x.x"
+1 -2
View File
@@ -91,7 +91,6 @@
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.17.30", "@types/node": "^20.17.30",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9", "@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3", "@types/picomatch": "^2.3.3",
@@ -208,10 +207,10 @@
"ora": "^7.0.1", "ora": "^7.0.1",
"oracledb": "^6.4.0", "oracledb": "^6.4.0",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0", "passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1", "passport-ldapauth": "^3.0.1",
"passport-oauth2": "^1.8.0",
"pg": "^8.11.3", "pg": "^8.11.3",
"pg-boss": "^10.1.5", "pg-boss": "^10.1.5",
"pg-query-stream": "^4.5.3", "pg-query-stream": "^4.5.3",
@@ -688,7 +688,11 @@ export const oidcConfigServiceFactory = ({
const strategy = new OpenIdStrategy( const strategy = new OpenIdStrategy(
{ {
client, client,
passReqToCallback: true passReqToCallback: true,
usePKCE: true,
params: {
code_challenge_method: "S256"
}
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(_req: any, tokenSet: TokenSet, cb: any) => { (_req: any, tokenSet: TokenSet, cb: any) => {
+14
View File
@@ -16,3 +16,17 @@ export const fetchGithubEmails = async (accessToken: string) => {
}); });
return data; return data;
}; };
type TGithubUser = {
name?: string;
login: string;
};
export const fetchGithubUser = async (accessToken: string) => {
const { data } = await request.get<TGithubUser>(`${INTEGRATION_GITHUB_API_URL}/user`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
return data;
};
+31 -18
View File
@@ -9,15 +9,15 @@
import { Authenticator } from "@fastify/passport"; import { Authenticator } from "@fastify/passport";
import fastifySession from "@fastify/session"; import fastifySession from "@fastify/session";
import RedisStore from "connect-redis"; import RedisStore from "connect-redis";
import { Strategy as GitHubStrategy } from "passport-github";
import { Strategy as GitLabStrategy } from "passport-gitlab2"; import { Strategy as GitLabStrategy } from "passport-gitlab2";
import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as OAuth2Strategy } from "passport-oauth2";
import { z } from "zod"; import { z } from "zod";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { fetchGithubEmails } from "@app/lib/requests/github"; import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
import { authRateLimit } from "@app/server/config/rateLimiter"; import { authRateLimit } from "@app/server/config/rateLimiter";
import { AuthMethod } from "@app/services/auth/auth-type"; import { AuthMethod } from "@app/services/auth/auth-type";
import { OrgAuthMethod } from "@app/services/org/org-types"; import { OrgAuthMethod } from "@app/services/org/org-types";
@@ -42,6 +42,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
}); });
await server.register(passport.initialize()); await server.register(passport.initialize());
await server.register(passport.secureSession()); await server.register(passport.secureSession());
// passport oauth strategy for Google // passport oauth strategy for Google
const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN); const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN);
if (isGoogleOauthActive) { if (isGoogleOauthActive) {
@@ -53,7 +54,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string, clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`,
scope: ["profile", " email"], scope: ["profile", " email"],
state: true state: true,
pkce: true
}, },
// eslint-disable-next-line // eslint-disable-next-line
async (req, _accessToken, _refreshToken, profile, cb) => { async (req, _accessToken, _refreshToken, profile, cb) => {
@@ -89,34 +91,44 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
const isGithubOauthActive = Boolean(appCfg.CLIENT_SECRET_GITHUB_LOGIN && appCfg.CLIENT_ID_GITHUB_LOGIN); const isGithubOauthActive = Boolean(appCfg.CLIENT_SECRET_GITHUB_LOGIN && appCfg.CLIENT_ID_GITHUB_LOGIN);
if (isGithubOauthActive) { if (isGithubOauthActive) {
passport.use( passport.use(
new GitHubStrategy( "github",
new OAuth2Strategy(
{ {
passReqToCallback: true, authorizationURL: "https://github.com/login/oauth/authorize",
clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string, tokenURL: "https://github.com/login/oauth/access_token",
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string, clientID: appCfg.CLIENT_ID_GITHUB_LOGIN!,
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN!,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`, 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,
state: true as unknown as string pkce: true,
passReqToCallback: true
}, },
// eslint-disable-next-line // eslint-disable-next-line
async (req, accessToken, _refreshToken, profile, cb) => { async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => {
// @ts-expect-error this is because this is express type and not fastify
const callbackPort = req.session.get("callbackPort");
try { try {
const ghEmails = await fetchGithubEmails(accessToken); const ghEmails = await fetchGithubEmails(accessToken);
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0]; const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
if (!email) throw new Error("No primary email found");
// profile does not get automatically populated so we need to manually fetch user info
const user = await fetchGithubUser(accessToken);
const callbackPort = req.session.get("callbackPort");
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
email, email,
firstName: profile.displayName || profile.username || "", firstName: user.name || user.login,
lastName: "", lastName: "",
authMethod: AuthMethod.GITHUB, authMethod: AuthMethod.GITHUB,
callbackPort callbackPort
}); });
return cb(null, { isUserCompleted, providerAuthToken });
} catch (error) { done(null, { isUserCompleted, providerAuthToken });
logger.error(error); } catch (err) {
cb(error as Error, false); logger.error(err);
done(err as Error, false);
} }
} }
) )
@@ -136,7 +148,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN, clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`,
baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL, baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL,
state: true state: true,
pkce: true
}, },
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => { async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
try { try {
@@ -12,6 +12,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getUserAgentType } from "@app/server/plugins/audit-log";
import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -39,7 +40,6 @@ import {
AuthTokenType, AuthTokenType,
MfaMethod MfaMethod
} from "./auth-type"; } from "./auth-type";
import { removeTrailingSlash } from "@app/lib/fn";
type TAuthLoginServiceFactoryDep = { type TAuthLoginServiceFactoryDep = {
userDAL: TUserDALFactory; userDAL: TUserDALFactory;