2025-05-28 23:08:42 +00:00
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" ;
2025-06-04 02:28:38 +00:00
import { OAuth2Client } from "google-auth-library" ;
import Credentials from "next-auth/providers/credentials" ;
import type { User as AuthJsUser } from "next-auth" ;
2025-11-05 04:08:04 +00:00
import type { Provider } from "next-auth/providers" ;
2025-06-04 02:28:38 +00:00
import { onCreateUser } from "@/lib/authUtils" ;
import { createLogger } from "@sourcebot/logger" ;
2025-11-05 04:08:04 +00:00
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" ;
2025-06-04 02:28:38 +00:00
const logger = createLogger ( 'web-sso' ) ;
2025-05-28 23:08:42 +00:00
2025-11-05 04:08:04 +00:00
const GITHUB_CLOUD_HOSTNAME = "github.com"
2025-05-28 23:08:42 +00:00
2025-11-05 04:08:04 +00:00
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 } ) ;
}
2025-05-28 23:08:42 +00:00
}
2025-11-05 04:08:04 +00:00
// @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 ( ' ' ) ,
2025-05-28 23:08:42 +00:00
} ,
2025-11-05 04:08:04 +00:00
} ,
} ) ;
}
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 ( ' ' ) ,
2025-05-28 23:08:42 +00:00
} ,
2025-11-05 04:08:04 +00:00
} ,
token : {
url : ` ${ url } /oauth/token ` ,
} ,
userinfo : {
url : ` ${ url } /api/v4/user ` ,
} ,
} ) ;
}
2025-05-28 23:08:42 +00:00
2025-11-05 04:08:04 +00:00
const createGoogleProvider = ( clientId : string , clientSecret : string ) : Provider = > {
return Google ( {
clientId : clientId ,
clientSecret : clientSecret ,
} ) ;
}
2025-05-28 23:08:42 +00:00
2025-11-05 04:08:04 +00:00
const createOktaProvider = ( clientId : string , clientSecret : string , issuer : string ) : Provider = > {
return Okta ( {
clientId : clientId ,
clientSecret : clientSecret ,
issuer : issuer ,
} ) ;
}
2025-05-28 23:08:42 +00:00
2025-11-05 04:08:04 +00:00
const createKeycloakProvider = ( clientId : string , clientSecret : string , issuer : string ) : Provider = > {
return Keycloak ( {
clientId : clientId ,
clientSecret : clientSecret ,
issuer : issuer ,
} ) ;
}
2025-05-28 23:08:42 +00:00
2025-11-05 04:08:04 +00:00
const createMicrosoftEntraIDProvider = ( clientId : string , clientSecret : string , issuer : string ) : Provider = > {
return MicrosoftEntraID ( {
clientId : clientId ,
clientSecret : clientSecret ,
issuer : issuer ,
} ) ;
}
2025-05-28 23:08:42 +00:00
2025-11-05 04:08:04 +00:00
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' ]
) ;
2025-06-04 02:28:38 +00:00
2025-11-05 04:08:04 +00:00
const payload = ticket . getPayload ( ) ;
if ( ! payload ) {
logger . warn ( "Invalid IAP token payload" ) ;
2025-06-04 02:28:38 +00:00
return null ;
}
2025-11-05 04:08:04 +00:00
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 ;
}
} ,
} ) ;
2025-07-15 03:14:41 +00:00
}