mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-15 05:45:20 +00:00
wip using identityprovider from config
This commit is contained in:
parent
6bc03f7a0e
commit
d4cf329af2
4 changed files with 195 additions and 149 deletions
|
|
@ -20,6 +20,7 @@ import { getAuditService } from '@/ee/features/audit/factory';
|
||||||
import { SINGLE_TENANT_ORG_ID } from './lib/constants';
|
import { SINGLE_TENANT_ORG_ID } from './lib/constants';
|
||||||
|
|
||||||
const auditService = getAuditService();
|
const auditService = getAuditService();
|
||||||
|
const ssoProviders = hasEntitlement("sso") ? await getSSOProviders() : [];
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
|
@ -38,11 +39,7 @@ declare module 'next-auth/jwt' {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getProviders = () => {
|
export const getProviders = () => {
|
||||||
const providers: Provider[] = [];
|
const providers: Provider[] = ssoProviders;
|
||||||
|
|
||||||
if (hasEntitlement("sso")) {
|
|
||||||
providers.push(...getSSOProviders());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS && env.AUTH_EMAIL_CODE_LOGIN_ENABLED === 'true') {
|
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS && env.AUTH_EMAIL_CODE_LOGIN_ENABLED === 'true') {
|
||||||
providers.push(EmailProvider({
|
providers.push(EmailProvider({
|
||||||
|
|
|
||||||
|
|
@ -12,172 +12,221 @@ import Credentials from "next-auth/providers/credentials";
|
||||||
import type { User as AuthJsUser } from "next-auth";
|
import type { User as AuthJsUser } from "next-auth";
|
||||||
import { onCreateUser } from "@/lib/authUtils";
|
import { onCreateUser } from "@/lib/authUtils";
|
||||||
import { createLogger } from "@sourcebot/logger";
|
import { createLogger } from "@sourcebot/logger";
|
||||||
import { hasEntitlement } from "@sourcebot/shared";
|
import { hasEntitlement, loadConfig } from "@sourcebot/shared";
|
||||||
|
import { getTokenFromConfig } from "@sourcebot/crypto";
|
||||||
|
import { SINGLE_TENANT_ORG_ID } from "@/lib/constants";
|
||||||
|
|
||||||
const logger = createLogger('web-sso');
|
const logger = createLogger('web-sso');
|
||||||
|
|
||||||
export const getSSOProviders = (): Provider[] => {
|
export const getSSOProviders = async (): Promise<Provider[]> => {
|
||||||
const providers: Provider[] = [];
|
const providers: Provider[] = [];
|
||||||
|
|
||||||
|
const config = env.CONFIG_PATH ? await loadConfig(env.CONFIG_PATH) : undefined;
|
||||||
|
const identityProviders = config?.identityProviders ?? [];
|
||||||
|
|
||||||
|
for (const identityProvider of identityProviders) {
|
||||||
|
if (identityProvider.provider === "github") {
|
||||||
|
const clientId = await getTokenFromConfig(identityProvider.clientId, SINGLE_TENANT_ORG_ID, db);
|
||||||
|
const clientSecret = await getTokenFromConfig(identityProvider.clientSecret, SINGLE_TENANT_ORG_ID, db);
|
||||||
|
const baseUrl = identityProvider.baseUrl ? await getTokenFromConfig(identityProvider.baseUrl, SINGLE_TENANT_ORG_ID, db) : undefined;
|
||||||
|
providers.push(createGitHubProvider(clientId, clientSecret, baseUrl));
|
||||||
|
}
|
||||||
|
if (identityProvider.provider === "gitlab") {
|
||||||
|
const clientId = await getTokenFromConfig(identityProvider.clientId, SINGLE_TENANT_ORG_ID, db);
|
||||||
|
const clientSecret = await getTokenFromConfig(identityProvider.clientSecret, SINGLE_TENANT_ORG_ID, db);
|
||||||
|
const baseUrl = identityProvider.baseUrl ? await getTokenFromConfig(identityProvider.baseUrl, SINGLE_TENANT_ORG_ID, db) : undefined;
|
||||||
|
providers.push(createGitLabProvider(clientId, clientSecret, baseUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_GITHUB_CLIENT_ID && env.AUTH_EE_GITHUB_CLIENT_SECRET) {
|
if (env.AUTH_EE_GITHUB_CLIENT_ID && env.AUTH_EE_GITHUB_CLIENT_SECRET) {
|
||||||
providers.push(GitHub({
|
providers.push(createGitHubProvider(env.AUTH_EE_GITHUB_CLIENT_ID, env.AUTH_EE_GITHUB_CLIENT_SECRET, env.AUTH_EE_GITHUB_BASE_URL));
|
||||||
clientId: env.AUTH_EE_GITHUB_CLIENT_ID,
|
|
||||||
clientSecret: env.AUTH_EE_GITHUB_CLIENT_SECRET,
|
|
||||||
enterprise: {
|
|
||||||
baseUrl: env.AUTH_EE_GITHUB_BASE_URL,
|
|
||||||
},
|
|
||||||
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(' '),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_GITLAB_CLIENT_ID && env.AUTH_EE_GITLAB_CLIENT_SECRET) {
|
if (env.AUTH_EE_GITLAB_CLIENT_ID && env.AUTH_EE_GITLAB_CLIENT_SECRET) {
|
||||||
providers.push(Gitlab({
|
providers.push(createGitLabProvider(env.AUTH_EE_GITLAB_CLIENT_ID, env.AUTH_EE_GITLAB_CLIENT_SECRET, env.AUTH_EE_GITLAB_BASE_URL));
|
||||||
clientId: env.AUTH_EE_GITLAB_CLIENT_ID,
|
|
||||||
clientSecret: env.AUTH_EE_GITLAB_CLIENT_SECRET,
|
|
||||||
authorization: {
|
|
||||||
url: `${env.AUTH_EE_GITLAB_BASE_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: `${env.AUTH_EE_GITLAB_BASE_URL}/oauth/token`,
|
|
||||||
},
|
|
||||||
userinfo: {
|
|
||||||
url: `${env.AUTH_EE_GITLAB_BASE_URL}/api/v4/user`,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_GOOGLE_CLIENT_ID && env.AUTH_EE_GOOGLE_CLIENT_SECRET) {
|
if (env.AUTH_EE_GOOGLE_CLIENT_ID && env.AUTH_EE_GOOGLE_CLIENT_SECRET) {
|
||||||
providers.push(Google({
|
providers.push(createGoogleProvider(env.AUTH_EE_GOOGLE_CLIENT_ID, env.AUTH_EE_GOOGLE_CLIENT_SECRET));
|
||||||
clientId: env.AUTH_EE_GOOGLE_CLIENT_ID,
|
|
||||||
clientSecret: env.AUTH_EE_GOOGLE_CLIENT_SECRET,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_OKTA_CLIENT_ID && env.AUTH_EE_OKTA_CLIENT_SECRET && env.AUTH_EE_OKTA_ISSUER) {
|
if (env.AUTH_EE_OKTA_CLIENT_ID && env.AUTH_EE_OKTA_CLIENT_SECRET && env.AUTH_EE_OKTA_ISSUER) {
|
||||||
providers.push(Okta({
|
providers.push(createOktaProvider(env.AUTH_EE_OKTA_CLIENT_ID, env.AUTH_EE_OKTA_CLIENT_SECRET, env.AUTH_EE_OKTA_ISSUER));
|
||||||
clientId: env.AUTH_EE_OKTA_CLIENT_ID,
|
|
||||||
clientSecret: env.AUTH_EE_OKTA_CLIENT_SECRET,
|
|
||||||
issuer: env.AUTH_EE_OKTA_ISSUER,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_KEYCLOAK_CLIENT_ID && env.AUTH_EE_KEYCLOAK_CLIENT_SECRET && env.AUTH_EE_KEYCLOAK_ISSUER) {
|
if (env.AUTH_EE_KEYCLOAK_CLIENT_ID && env.AUTH_EE_KEYCLOAK_CLIENT_SECRET && env.AUTH_EE_KEYCLOAK_ISSUER) {
|
||||||
providers.push(Keycloak({
|
providers.push(createKeycloakProvider(env.AUTH_EE_KEYCLOAK_CLIENT_ID, env.AUTH_EE_KEYCLOAK_CLIENT_SECRET, env.AUTH_EE_KEYCLOAK_ISSUER));
|
||||||
clientId: env.AUTH_EE_KEYCLOAK_CLIENT_ID,
|
|
||||||
clientSecret: env.AUTH_EE_KEYCLOAK_CLIENT_SECRET,
|
|
||||||
issuer: env.AUTH_EE_KEYCLOAK_ISSUER,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID && env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET && env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER) {
|
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(MicrosoftEntraID({
|
providers.push(createMicrosoftEntraIDProvider(env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID, env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET, env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER));
|
||||||
clientId: env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_ID,
|
|
||||||
clientSecret: env.AUTH_EE_MICROSOFT_ENTRA_ID_CLIENT_SECRET,
|
|
||||||
issuer: env.AUTH_EE_MICROSOFT_ENTRA_ID_ISSUER,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_EE_GCP_IAP_ENABLED && env.AUTH_EE_GCP_IAP_AUDIENCE) {
|
if (env.AUTH_EE_GCP_IAP_ENABLED && env.AUTH_EE_GCP_IAP_AUDIENCE) {
|
||||||
providers.push(Credentials({
|
providers.push(createGCPIAPProvider(env.AUTH_EE_GCP_IAP_AUDIENCE));
|
||||||
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,
|
|
||||||
env.AUTH_EE_GCP_IAP_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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return providers;
|
return providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createGitHubProvider = (clientId: string, clientSecret: string, baseUrl?: string): Provider => {
|
||||||
|
return GitHub({
|
||||||
|
clientId: clientId,
|
||||||
|
clientSecret: clientSecret,
|
||||||
|
enterprise: {
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"const": "github"
|
"const": "github"
|
||||||
},
|
},
|
||||||
"purpose": {
|
"purpose": {
|
||||||
"enum": ["sso", "identity"]
|
"enum": ["sso", "integration"]
|
||||||
},
|
},
|
||||||
"clientId": {
|
"clientId": {
|
||||||
"$ref": "./shared.json#/definitions/Token"
|
"$ref": "./shared.json#/definitions/Token"
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"const": "gitlab"
|
"const": "gitlab"
|
||||||
},
|
},
|
||||||
"purpose": {
|
"purpose": {
|
||||||
"enum": ["sso", "identity"]
|
"enum": ["sso", "integration"]
|
||||||
},
|
},
|
||||||
"clientId": {
|
"clientId": {
|
||||||
"$ref": "./shared.json#/definitions/Token"
|
"$ref": "./shared.json#/definitions/Token"
|
||||||
|
|
@ -130,7 +130,7 @@
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Defines a collection of identity providers that are available to Sourcebot.",
|
"description": "Defines a collection of identity providers that are available to Sourcebot.",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "./authProvider.json"
|
"$ref": "./identityProvider.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue