Files
speckle-server/packages/server/modules/workspaces/helpers/sso.ts
T
Kristaps Fabians Geikins bde148f286 chore(server): migrating fully to ESM (#5042)
* wip

* some extra fixes

* stuff kinda works?

* need to figure out mocks

* need to figure out mocks

* fix db listener

* gqlgen fix

* minor gqlgen watch adjustment

* lint fixes

* delete old codegen file

* converting migrations to ESM

* getModuleDIrectory

* vitest sort of works

* added back ts-vitest

* resolve gql double load

* fixing test timeout configs

* TSC lint fix

* fix automate tests

* moar debugging

* debugging

* more debugging

* codegen update

* server works

* yargs migrated

* chore(server): getting rid of global mocks for Server ESM (#5046)

* got rid of email mock

* got rid of comment mocks

* got rid of multi region mocks

* got rid of stripe mock

* admin override mock updated

* removed final mock

* fixing import.meta.resolve calls

* another import.meta.resolve fix

* added requested test

* nyc ESM fix

* removed unneeded deps + linting

* yarn lock forgot to commit

* tryna fix flakyness

* email capture util fix

* sendEmail fix

* fix TSX check

* sender transporter fix + CR comments

* merge main fix

* test fixx

* circleci fix

* gqlgen bigint fix

* error formatter fix

* more error formatting improvements

* esmloader added to Dockerfile

* more dockerfile fixes

* bg jobs fix
2025-07-14 10:26:19 +03:00

126 lines
3.6 KiB
TypeScript

import { getEncryptionKeyPair } from '@/modules/automate/services/encryption'
import { base64Decode } from '@/modules/shared/helpers/cryptoHelper'
import { getFrontendOrigin, getServerOrigin } from '@/modules/shared/helpers/envHelper'
import { buildDecryptor, buildEncryptor } from '@/modules/shared/utils/libsodium'
import { SsoSessionState } from '@/modules/workspaces/domain/sso/types'
import {
OidcStateInvalidError,
OidcStateMissingError,
SsoVerificationCodeMissingError
} from '@/modules/workspaces/errors/sso'
import { OidcProvider } from '@/modules/workspaces/domain/sso/types'
import { Request } from 'express'
import { omit } from 'lodash-es'
declare module 'express-session' {
interface SessionData {
workspaceId?: string
workspaceSlug?: string
ssoNonce?: string
ssoState?: SsoSessionState
oidcProvider?: OidcProvider
}
}
/**
* Generate Speckle URL to redirect users to after they complete authorization
* with the given SSO provider.
*/
export const buildAuthRedirectUrl = (workspaceSlug: string): URL => {
const url = new URL(
`/api/v1/workspaces/${workspaceSlug}/sso/oidc/callback`,
getServerOrigin()
)
return url
}
export const buildAuthFinalizeRedirectUrl = (
workspaceSlug: string,
searchParams: Record<string, string> = {}
) => {
const url = new URL(`/workspaces/${workspaceSlug}/sso`, getFrontendOrigin())
for (const [key, value] of Object.entries(searchParams)) {
url.searchParams.set(key, value)
}
return url
}
export const buildAuthErrorRedirectUrl = (workspaceSlug: string, error: string) => {
return buildAuthFinalizeRedirectUrl(workspaceSlug, {
ssoError: error
})
}
export const buildValidationErrorRedirectUrl = (
workspaceSlug: string,
error: string,
oidcProvider?: OidcProvider
) => {
const url = new URL(
`/settings/workspaces/${workspaceSlug}/security`,
getFrontendOrigin()
)
url.searchParams.set('ssoValidationSuccess', 'false')
url.searchParams.set('ssoError', error)
for (const [key, value] of Object.entries<string>(
omit(oidcProvider ?? {}, 'clientSecret')
)) {
url.searchParams.set(key, value)
}
return url
}
export const getErrorMessage = (e: unknown): string => {
return e instanceof Error ? `${e.message}` : `Unknown error: ${JSON.stringify(e)}`
}
export const getEncryptor = () => async (data: string) => {
const encryptionKeyPair = await getEncryptionKeyPair()
const encryptor = await buildEncryptor(encryptionKeyPair.publicKey)
const encryptedData = await encryptor.encrypt(data)
encryptor.dispose()
return encryptedData
}
export const getDecryptor = () => async (data: string) => {
const encryptionKeyPair = await getEncryptionKeyPair()
const decryptor = await buildDecryptor(encryptionKeyPair)
const decryptedData = await decryptor.decrypt(data)
decryptor.dispose()
return decryptedData
}
export const parseCodeVerifier = async (req: Request<unknown>): Promise<string> => {
const encryptedCodeVerifier = req.session.codeVerifier
if (!encryptedCodeVerifier) throw new SsoVerificationCodeMissingError()
const codeVerifier = await getDecryptor()(encryptedCodeVerifier)
return codeVerifier
}
export const getSsoSessionState = (
req: Request<unknown, unknown, unknown, { state: string }>
): SsoSessionState => {
const sessionNonce = req.session.ssoNonce
const requestNonce = base64Decode(req.query.state)
if (!sessionNonce || !requestNonce || sessionNonce !== requestNonce) {
throw new OidcStateInvalidError()
}
const state = req.session.ssoState
if (!state) {
throw new OidcStateMissingError()
}
return state
}