mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 21:18:18 +00:00
feat(infisical-pg): added github,gitlab and google oauth
This commit is contained in:
Vendored
+5
@@ -29,6 +29,11 @@ declare module "fastify" {
|
||||
tokenVersionId: string; // the session id of token used
|
||||
user: TUsers;
|
||||
};
|
||||
// passport data
|
||||
passportUser: {
|
||||
isCompleted: string;
|
||||
providerAuthToken: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface FastifyInstance {
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
declare module "passport-gitlab2";
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
import { AuthMethod } from "../../services/auth/auth-type";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export const testUser = {
|
||||
email: "[email protected]"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "../schemas";
|
||||
import { testUser } from "./1-user";
|
||||
|
||||
|
||||
@@ -27,14 +27,26 @@ const envSchema = z
|
||||
SMTP_PASSWORD: zpStr(z.string().optional()),
|
||||
SMTP_FROM_ADDRESS: zpStr(z.string().optional()),
|
||||
SMTP_FROM_NAME: zpStr(z.string().optional().default("Infisical")),
|
||||
COOKIE_SECRET_SIGN_KEY: z.string().default("g5giLbOMpaJhqEogXApkiw2ZFW5Q0jvA"),
|
||||
COOKIE_SECRET_SIGN_KEY: z
|
||||
.string()
|
||||
.min(32)
|
||||
.default("#5VihU%rbXHcHwWwCot5L3vyPsx$7dWYw^iGk!EJg2bC*f$PD$%KCqx^R@#^LSEf"),
|
||||
SITE_URL: zpStr(z.string().optional()),
|
||||
// jwt options
|
||||
JWT_AUTH_SECRET: zpStr(z.string()),
|
||||
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
|
||||
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
|
||||
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
|
||||
JWT_MFA_LIFETIME: zpStr(z.string().default("5m"))
|
||||
JWT_MFA_LIFETIME: zpStr(z.string().default("5m")),
|
||||
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
|
||||
// Oauth
|
||||
CLIENT_ID_GOOGLE_LOGIN: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_GOOGLE_LOGIN: zpStr(z.string().optional()),
|
||||
CLIENT_ID_GITHUB_LOGIN: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_GITHUB_LOGIN: zpStr(z.string().optional()),
|
||||
CLIENT_ID_GITLAB_LOGIN: zpStr(z.string().optional()),
|
||||
CLIENT_SECRET_GITLAB_LOGIN: zpStr(z.string().optional()),
|
||||
CLIENT_GITLAB_LOGIN_URL: zpStr(z.string().optional())
|
||||
})
|
||||
.transform((data) => ({ ...data, isSmtpConfigured: Boolean(data.SMTP_HOST) }));
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import axios from "axios";
|
||||
import axiosRetry from "axios-retry";
|
||||
|
||||
export const request = axios.create();
|
||||
|
||||
axiosRetry(request, {
|
||||
retries: 3,
|
||||
retryDelay: axiosRetry.exponentialDelay,
|
||||
retryCondition: (err) => axiosRetry.isNetworkError(err) || axiosRetry.isRetryableError(err)
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { request } from "../config/request";
|
||||
|
||||
const INTEGRATION_GITHUB_API_URL = "https://api.github.com";
|
||||
type TGithubEmail = {
|
||||
email: string;
|
||||
primary: boolean;
|
||||
verified: boolean;
|
||||
visibility: null | string;
|
||||
};
|
||||
|
||||
export const fetchGithubEmails = async (accessToken: string) => {
|
||||
const { data } = await request.get<TGithubEmail[]>(`${INTEGRATION_GITHUB_API_URL}/user/emails`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
return data;
|
||||
};
|
||||
@@ -3,11 +3,14 @@ import { registerAuthRoutes } from "./auth-router";
|
||||
import { registerInviteOrgRouter } from "./invite-org-router";
|
||||
import { registerOrgRouter } from "./organization-router";
|
||||
import { registerPasswordRouter } from "./password-router";
|
||||
import { registerSsoRouter } from "./sso-router";
|
||||
import { registerUserActionRouter } from "./user-action-router";
|
||||
import { registerUserRouter } from "./user-router";
|
||||
|
||||
export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await server.register(registerSsoRouter, { prefix: "/sso" });
|
||||
await server.register(registerAuthRoutes, { prefix: "/auth" });
|
||||
// await server.register(registerSSORouter, { prefix: "/sso" });
|
||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||
await server.register(registerOrgRouter, { prefix: "/organization" });
|
||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
import { Strategy as GitHubStrategy } from "passport-github";
|
||||
import { Strategy as GitLabStrategy } from "passport-gitlab2";
|
||||
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
|
||||
import { z } from "zod";
|
||||
import { Authenticator } from "@fastify/passport";
|
||||
import fastifySession from "@fastify/session";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { fetchGithubEmails } from "@app/lib/requests/github";
|
||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
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 });
|
||||
await server.register(passport.initialize());
|
||||
await server.register(passport.secureSession());
|
||||
|
||||
// passport oauth strategy for Google
|
||||
const isGoogleOauthActive = Boolean(
|
||||
appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN
|
||||
);
|
||||
if (isGoogleOauthActive) {
|
||||
passport.use(
|
||||
new GoogleStrategy(
|
||||
{
|
||||
passReqToCallback: true,
|
||||
clientID: appCfg.CLIENT_ID_GOOGLE_LOGIN as string,
|
||||
clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string,
|
||||
callbackURL: "/api/v1/sso/google",
|
||||
scope: ["profile", " email"]
|
||||
},
|
||||
async (req, _accessToken, _refreshToken, profile, cb) => {
|
||||
try {
|
||||
const email = profile?.emails?.[0]?.value;
|
||||
const serverCfg = server.services.superAdmin.getServerCfg();
|
||||
if (!email)
|
||||
throw new BadRequestError({
|
||||
message: "Email not found",
|
||||
name: "Oauth Google Register"
|
||||
});
|
||||
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile?.name?.givenName || "",
|
||||
lastName: profile?.name?.familyName || "",
|
||||
authMethod: AuthMethod.GOOGLE,
|
||||
callbackPort: req.query.state as string,
|
||||
isSignupAllowed: Boolean(serverCfg.allowSignUp)
|
||||
});
|
||||
cb(null, { isUserCompleted, providerAuthToken });
|
||||
} catch (error) {
|
||||
cb(null, false);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Passport strategy for Github
|
||||
const isGithubOauthActive = Boolean(
|
||||
appCfg.CLIENT_SECRET_GITHUB_LOGIN && appCfg.CLIENT_ID_GITLAB_LOGIN
|
||||
);
|
||||
if (isGithubOauthActive) {
|
||||
passport.use(
|
||||
new GitHubStrategy(
|
||||
{
|
||||
passReqToCallback: true,
|
||||
clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string,
|
||||
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string,
|
||||
callbackURL: "/api/v1/sso/github",
|
||||
scope: ["user:email"]
|
||||
},
|
||||
async (req, accessToken, _refreshToken, profile, cb) => {
|
||||
const ghEmails = await fetchGithubEmails(accessToken);
|
||||
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
||||
|
||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
||||
email,
|
||||
firstName: profile.displayName,
|
||||
lastName: "",
|
||||
authMethod: AuthMethod.GITHUB,
|
||||
callbackPort: req.query.state as string
|
||||
});
|
||||
|
||||
return cb(null, { isUserCompleted, providerAuthToken });
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// passport strategy for gitlab
|
||||
const isGitlabOauthActive = Boolean(
|
||||
appCfg.CLIENT_ID_GITLAB_LOGIN &&
|
||||
appCfg.CLIENT_SECRET_GITLAB_LOGIN &&
|
||||
appCfg.CLIENT_GITLAB_LOGIN_URL
|
||||
);
|
||||
if (isGitlabOauthActive) {
|
||||
passport.use(
|
||||
new GitLabStrategy(
|
||||
{
|
||||
passReqToCallback: true,
|
||||
clientID: appCfg.CLIENT_ID_GITLAB_LOGIN,
|
||||
clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN,
|
||||
callbackURL: "/api/v1/sso/gitlab",
|
||||
baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL
|
||||
},
|
||||
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
|
||||
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
|
||||
});
|
||||
|
||||
return cb(null, { isUserCompleted, providerAuthToken });
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
server.route({
|
||||
url: "/sso/redirect/google",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
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
|
||||
}) as any
|
||||
)(req, res),
|
||||
handler: () => {}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/sso/google",
|
||||
method: "GET",
|
||||
preValidation: passport.authenticate("google", {
|
||||
session: false,
|
||||
failureRedirect: "/login/provider/error",
|
||||
authInfo: false
|
||||
// this is due to zod type difference
|
||||
}) as any,
|
||||
handler: (req, res) => {
|
||||
if (req.passportUser.isCompleted) {
|
||||
return res.redirect(
|
||||
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(
|
||||
req.passportUser.providerAuthToken
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return res.redirect(
|
||||
`${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(
|
||||
req.passportUser.providerAuthToken
|
||||
)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/sso/redirect/github",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
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),
|
||||
handler: () => {}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/sso/github",
|
||||
method: "GET",
|
||||
preValidation: passport.authenticate("github", {
|
||||
session: false,
|
||||
failureRedirect: "/login/provider/error",
|
||||
authInfo: false
|
||||
// this is due to zod type difference
|
||||
}) as any,
|
||||
handler: (req, res) => {
|
||||
if (req.passportUser.isCompleted) {
|
||||
return res.redirect(
|
||||
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(
|
||||
req.passportUser.providerAuthToken
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return res.redirect(
|
||||
`${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(
|
||||
req.passportUser.providerAuthToken
|
||||
)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/sso/redirect/gitlab",
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
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
|
||||
}) as any
|
||||
)(req, res),
|
||||
handler: () => {}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/sso/gitlab",
|
||||
method: "GET",
|
||||
preValidation: passport.authenticate("gitlab", {
|
||||
session: false,
|
||||
failureRedirect: "/login/provider/error",
|
||||
authInfo: false
|
||||
// this is due to zod type difference
|
||||
}) as any,
|
||||
handler: (req, res) => {
|
||||
if (req.passportUser.isCompleted) {
|
||||
return res.redirect(
|
||||
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(
|
||||
req.passportUser.providerAuthToken
|
||||
)}`
|
||||
);
|
||||
}
|
||||
return res.redirect(
|
||||
`${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(
|
||||
req.passportUser.providerAuthToken
|
||||
)}`
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import jwt from "jsonwebtoken";
|
||||
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
@@ -11,6 +12,7 @@ import { TUserDalFactory } from "../user/user-dal";
|
||||
import {
|
||||
TLoginClientProofDTO,
|
||||
TLoginGenServerPublicKeyDTO,
|
||||
TOauthLoginDTO,
|
||||
TVerifyMfaTokenDTO
|
||||
} from "./auth-login-type";
|
||||
import { AuthMethod, AuthTokenType } from "./auth-type";
|
||||
@@ -228,6 +230,53 @@ export const authLoginServiceFactory = ({
|
||||
const token = await generateUserTokens({ ...userEnc, id: userEnc.userId }, ip, userAgent);
|
||||
return { token, user: userEnc };
|
||||
};
|
||||
/*
|
||||
* OAuth2 login for google,github, and other oauth2 provider
|
||||
* */
|
||||
const oauth2Login = async ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
authMethod,
|
||||
callbackPort,
|
||||
isSignupAllowed
|
||||
}: TOauthLoginDTO) => {
|
||||
let user = await userDal.findUserByEmail(email);
|
||||
const appCfg = getConfig();
|
||||
const isOauthSignUpDisabled = !isSignupAllowed && !user;
|
||||
if (isOauthSignUpDisabled)
|
||||
throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" });
|
||||
|
||||
if (!user) {
|
||||
user = await userDal.create({ email, firstName, lastName, authMethods: [authMethod] });
|
||||
}
|
||||
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||
const isUserCompleted = user.isAccepted;
|
||||
|
||||
const providerAuthToken = jwt.sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
authMethod,
|
||||
isUserCompleted,
|
||||
isLinkingRequired,
|
||||
...(callbackPort
|
||||
? {
|
||||
callbackPort
|
||||
}
|
||||
: {})
|
||||
},
|
||||
appCfg.JWT_AUTH_SECRET,
|
||||
{
|
||||
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
||||
}
|
||||
);
|
||||
|
||||
return { isUserCompleted, providerAuthToken };
|
||||
};
|
||||
|
||||
/*
|
||||
* logout user by incrementing the version by 1 meaning any old session will become invalid
|
||||
@@ -241,6 +290,7 @@ export const authLoginServiceFactory = ({
|
||||
loginGenServerPublicKey,
|
||||
loginExchangeClientProof,
|
||||
logout,
|
||||
oauth2Login,
|
||||
resendMfaToken,
|
||||
verifyMfaToken,
|
||||
generateUserTokens
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { AuthMethod } from "./auth-type";
|
||||
|
||||
export type TLoginGenServerPublicKeyDTO = {
|
||||
email: string;
|
||||
clientPublicKey: string;
|
||||
@@ -18,3 +20,12 @@ export type TVerifyMfaTokenDTO = {
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
};
|
||||
|
||||
export type TOauthLoginDTO = {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
authMethod: AuthMethod;
|
||||
callbackPort?: string;
|
||||
isSignupAllowed?: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user