mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
274 lines
No EOL
13 KiB
TypeScript
274 lines
No EOL
13 KiB
TypeScript
import { env } from "@/env.mjs";
|
|
import GitHub from "next-auth/providers/github";
|
|
import Google from "next-auth/providers/google";
|
|
import Okta from "next-auth/providers/okta";
|
|
import Keycloak from "next-auth/providers/keycloak";
|
|
import Gitlab from "next-auth/providers/gitlab";
|
|
import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id";
|
|
import { prisma } from "@/prisma";
|
|
import { OAuth2Client } from "google-auth-library";
|
|
import Credentials from "next-auth/providers/credentials";
|
|
import type { User as AuthJsUser } from "next-auth";
|
|
import type { Provider } from "next-auth/providers";
|
|
import { onCreateUser } from "@/lib/authUtils";
|
|
import { createLogger } from "@sourcebot/logger";
|
|
import { hasEntitlement, loadConfig } from "@sourcebot/shared";
|
|
import { getTokenFromConfig } from "@sourcebot/crypto";
|
|
import type { IdentityProvider } from "@/auth";
|
|
import { GCPIAPIdentityProviderConfig, GitHubIdentityProviderConfig, GitLabIdentityProviderConfig, GoogleIdentityProviderConfig, KeycloakIdentityProviderConfig, MicrosoftEntraIDIdentityProviderConfig, OktaIdentityProviderConfig } from "@sourcebot/schemas/v3/index.type";
|
|
|
|
const logger = createLogger('web-sso');
|
|
|
|
const GITHUB_CLOUD_HOSTNAME = "github.com"
|
|
|
|
export const getEEIdentityProviders = async (): Promise<IdentityProvider[]> => {
|
|
const providers: IdentityProvider[] = [];
|
|
|
|
const config = await loadConfig(env.CONFIG_PATH);
|
|
const identityProviders = config?.identityProviders ?? [];
|
|
|
|
for (const identityProvider of identityProviders) {
|
|
if (identityProvider.provider === "github") {
|
|
const providerConfig = identityProvider as GitHubIdentityProviderConfig;
|
|
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
|
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
|
const baseUrl = providerConfig.baseUrl;
|
|
providers.push({ provider: createGitHubProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose, required: providerConfig.accountLinkingRequired ?? false});
|
|
}
|
|
if (identityProvider.provider === "gitlab") {
|
|
const providerConfig = identityProvider as GitLabIdentityProviderConfig;
|
|
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
|
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
|
const baseUrl = providerConfig.baseUrl;
|
|
providers.push({ provider: createGitLabProvider(clientId, clientSecret, baseUrl), purpose: providerConfig.purpose, required: providerConfig.accountLinkingRequired ?? false});
|
|
}
|
|
if (identityProvider.provider === "google") {
|
|
const providerConfig = identityProvider as GoogleIdentityProviderConfig;
|
|
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
|
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
|
providers.push({ provider: createGoogleProvider(clientId, clientSecret), purpose: providerConfig.purpose});
|
|
}
|
|
if (identityProvider.provider === "okta") {
|
|
const providerConfig = identityProvider as OktaIdentityProviderConfig;
|
|
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
|
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
|
const issuer = await getTokenFromConfig(providerConfig.issuer);
|
|
providers.push({ provider: createOktaProvider(clientId, clientSecret, issuer), purpose: providerConfig.purpose});
|
|
}
|
|
if (identityProvider.provider === "keycloak") {
|
|
const providerConfig = identityProvider as KeycloakIdentityProviderConfig;
|
|
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
|
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
|
const issuer = await getTokenFromConfig(providerConfig.issuer);
|
|
providers.push({ provider: createKeycloakProvider(clientId, clientSecret, issuer), purpose: providerConfig.purpose });
|
|
}
|
|
if (identityProvider.provider === "microsoft-entra-id") {
|
|
const providerConfig = identityProvider as MicrosoftEntraIDIdentityProviderConfig;
|
|
const clientId = await getTokenFromConfig(providerConfig.clientId);
|
|
const clientSecret = await getTokenFromConfig(providerConfig.clientSecret);
|
|
const issuer = await getTokenFromConfig(providerConfig.issuer);
|
|
providers.push({ provider: createMicrosoftEntraIDProvider(clientId, clientSecret, issuer), purpose: providerConfig.purpose });
|
|
}
|
|
if (identityProvider.provider === "gcp-iap") {
|
|
const providerConfig = identityProvider as GCPIAPIdentityProviderConfig;
|
|
const audience = await getTokenFromConfig(providerConfig.audience);
|
|
providers.push({ provider: createGCPIAPProvider(audience), purpose: providerConfig.purpose });
|
|
}
|
|
}
|
|
|
|
// @deprecate in favor of defining identity providers throught the identityProvider object in the config file. This was done to allow for more control over
|
|
// which identity providers are defined and their purpose. We've left this logic here to support backwards compat with deployments that expect these env vars,
|
|
// but this logic will be removed in the future
|
|
// We only go through this path if no identityProviders are defined in the config to prevent accidental duplication of providers
|
|
if (identityProviders.length == 0) {
|
|
if (env.AUTH_EE_GITHUB_CLIENT_ID && env.AUTH_EE_GITHUB_CLIENT_SECRET) {
|
|
providers.push({ provider: createGitHubProvider(env.AUTH_EE_GITHUB_CLIENT_ID, env.AUTH_EE_GITHUB_CLIENT_SECRET, env.AUTH_EE_GITHUB_BASE_URL), purpose: "sso" });
|
|
}
|
|
|
|
if (env.AUTH_EE_GITLAB_CLIENT_ID && env.AUTH_EE_GITLAB_CLIENT_SECRET) {
|
|
providers.push({ provider: createGitLabProvider(env.AUTH_EE_GITLAB_CLIENT_ID, env.AUTH_EE_GITLAB_CLIENT_SECRET, env.AUTH_EE_GITLAB_BASE_URL), purpose: "sso" });
|
|
}
|
|
|
|
if (env.AUTH_EE_GOOGLE_CLIENT_ID && env.AUTH_EE_GOOGLE_CLIENT_SECRET) {
|
|
providers.push({ provider: createGoogleProvider(env.AUTH_EE_GOOGLE_CLIENT_ID, env.AUTH_EE_GOOGLE_CLIENT_SECRET), purpose: "sso" });
|
|
}
|
|
|
|
if (env.AUTH_EE_OKTA_CLIENT_ID && env.AUTH_EE_OKTA_CLIENT_SECRET && env.AUTH_EE_OKTA_ISSUER) {
|
|
providers.push({ provider: createOktaProvider(env.AUTH_EE_OKTA_CLIENT_ID, env.AUTH_EE_OKTA_CLIENT_SECRET, env.AUTH_EE_OKTA_ISSUER), purpose: "sso" });
|
|
}
|
|
|
|
if (env.AUTH_EE_KEYCLOAK_CLIENT_ID && env.AUTH_EE_KEYCLOAK_CLIENT_SECRET && env.AUTH_EE_KEYCLOAK_ISSUER) {
|
|
providers.push({ provider: createKeycloakProvider(env.AUTH_EE_KEYCLOAK_CLIENT_ID, env.AUTH_EE_KEYCLOAK_CLIENT_SECRET, env.AUTH_EE_KEYCLOAK_ISSUER), purpose: "sso" });
|
|
}
|
|
|
|
if (env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID && env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET && env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER) {
|
|
providers.push({ provider: createMicrosoftEntraIDProvider(env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID, env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET, env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER), purpose: "sso" });
|
|
}
|
|
|
|
if (env.AUTH_EE_GCP_IAP_ENABLED && env.AUTH_EE_GCP_IAP_AUDIENCE) {
|
|
providers.push({ provider: createGCPIAPProvider(env.AUTH_EE_GCP_IAP_AUDIENCE), purpose: "sso" });
|
|
}
|
|
}
|
|
|
|
return providers;
|
|
}
|
|
|
|
const createGitHubProvider = (clientId: string, clientSecret: string, baseUrl?: string): Provider => {
|
|
const hostname = baseUrl ? new URL(baseUrl).hostname : GITHUB_CLOUD_HOSTNAME
|
|
return GitHub({
|
|
clientId: clientId,
|
|
clientSecret: clientSecret,
|
|
...(hostname === GITHUB_CLOUD_HOSTNAME ? { enterprise: { baseUrl: baseUrl } } : {}), // if this is set the provider expects GHE so we need this check
|
|
authorization: {
|
|
params: {
|
|
scope: [
|
|
'read:user',
|
|
'user:email',
|
|
// Permission syncing requires the `repo` scope in order to fetch repositories
|
|
// for the authenticated user.
|
|
// @see: https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-the-authenticated-user
|
|
...(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing') ?
|
|
['repo'] :
|
|
[]
|
|
),
|
|
].join(' '),
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
const createGitLabProvider = (clientId: string, clientSecret: string, baseUrl?: string): Provider => {
|
|
const url = baseUrl ?? 'https://gitlab.com';
|
|
return Gitlab({
|
|
clientId: clientId,
|
|
clientSecret: clientSecret,
|
|
authorization: {
|
|
url: `${url}/oauth/authorize`,
|
|
params: {
|
|
scope: [
|
|
"read_user",
|
|
// Permission syncing requires the `read_api` scope in order to fetch projects
|
|
// for the authenticated user and project members.
|
|
// @see: https://docs.gitlab.com/ee/api/projects.html#list-all-projects
|
|
...(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing') ?
|
|
['read_api'] :
|
|
[]
|
|
),
|
|
].join(' '),
|
|
},
|
|
},
|
|
token: {
|
|
url: `${url}/oauth/token`,
|
|
},
|
|
userinfo: {
|
|
url: `${url}/api/v4/user`,
|
|
},
|
|
});
|
|
}
|
|
|
|
const createGoogleProvider = (clientId: string, clientSecret: string): Provider => {
|
|
return Google({
|
|
clientId: clientId,
|
|
clientSecret: clientSecret,
|
|
});
|
|
}
|
|
|
|
const createOktaProvider = (clientId: string, clientSecret: string, issuer: string): Provider => {
|
|
return Okta({
|
|
clientId: clientId,
|
|
clientSecret: clientSecret,
|
|
issuer: issuer,
|
|
});
|
|
}
|
|
|
|
const createKeycloakProvider = (clientId: string, clientSecret: string, issuer: string): Provider => {
|
|
return Keycloak({
|
|
clientId: clientId,
|
|
clientSecret: clientSecret,
|
|
issuer: issuer,
|
|
});
|
|
}
|
|
|
|
const createMicrosoftEntraIDProvider = (clientId: string, clientSecret: string, issuer: string): Provider => {
|
|
return MicrosoftEntraID({
|
|
clientId: clientId,
|
|
clientSecret: clientSecret,
|
|
issuer: issuer,
|
|
});
|
|
}
|
|
|
|
const createGCPIAPProvider = (audience: string): Provider => {
|
|
return Credentials({
|
|
id: "gcp-iap",
|
|
name: "Google Cloud IAP",
|
|
credentials: {},
|
|
authorize: async (credentials, req) => {
|
|
try {
|
|
const iapAssertion = req.headers?.get("x-goog-iap-jwt-assertion");
|
|
if (!iapAssertion || typeof iapAssertion !== "string") {
|
|
logger.warn("No IAP assertion found in headers");
|
|
return null;
|
|
}
|
|
|
|
const oauth2Client = new OAuth2Client();
|
|
|
|
const { pubkeys } = await oauth2Client.getIapPublicKeys();
|
|
const ticket = await oauth2Client.verifySignedJwtWithCertsAsync(
|
|
iapAssertion,
|
|
pubkeys,
|
|
audience,
|
|
['https://cloud.google.com/iap']
|
|
);
|
|
|
|
const payload = ticket.getPayload();
|
|
if (!payload) {
|
|
logger.warn("Invalid IAP token payload");
|
|
return null;
|
|
}
|
|
|
|
const email = payload.email;
|
|
const name = payload.name || payload.email;
|
|
const image = payload.picture;
|
|
|
|
if (!email) {
|
|
logger.warn("Missing email in IAP token");
|
|
return null;
|
|
}
|
|
|
|
const existingUser = await prisma.user.findUnique({
|
|
where: { email }
|
|
});
|
|
|
|
if (!existingUser) {
|
|
const newUser = await prisma.user.create({
|
|
data: {
|
|
email,
|
|
name,
|
|
image,
|
|
}
|
|
});
|
|
|
|
const authJsUser: AuthJsUser = {
|
|
id: newUser.id,
|
|
email: newUser.email,
|
|
name: newUser.name,
|
|
image: newUser.image,
|
|
};
|
|
|
|
await onCreateUser({ user: authJsUser });
|
|
return authJsUser;
|
|
} else {
|
|
return {
|
|
id: existingUser.id,
|
|
email: existingUser.email,
|
|
name: existingUser.name,
|
|
image: existingUser.image,
|
|
};
|
|
}
|
|
} catch (error) {
|
|
logger.error("Error verifying IAP token:", error);
|
|
return null;
|
|
}
|
|
},
|
|
});
|
|
} |