Files
speckle-server/packages/server/modules/workspaces/clients/oidcProvider.ts
T
Chuck Driesler 52bb1116ed SSO (#3376)
* feat(workspaces): add workspace sso feature flag

* feat(workspaceSso): wip validate sso

* feat(workspaces): validate and add sso provider to the workspace with user sso sessions

* feat(workspaces): validate and add sso provider to the workspace with user sso sessions

* WIP

* fix(sso): restructure to handle all branches at end of flow

* fix(sso): add and validate emails used for sso

* fix(sso): park progress

* chore(workspaces): review sso login/valdate

* fix(sso): adjust validate url

* chore(sso): auth header puzzle

* fix(sso): happy-path config

* chore(gql): gqlgen

* fix(sso): almost almost

* fix(sso): auth endpoint

* a lil more terse

* fix(sso): light at the end of the tunnel

* fix(sso): improve catch block error messages

* fix(sso): session lifespan => validUntil

* fix(sso): I think we've got it

* feat(sso): limited workspace values for public sso login

* fix(sso): use factory functions

* fix(sso): til decrypt is single-use

* fix(sso): correct usage of access codes

* fix(sso): use finalize middleware in all routes

* chore(sso): cheeky tweak

* fix(sso): move some types around

* fix(sso): stencil final shape I'm sleepy

* fix(sso): more factories more factories

* fix(sso): on to final boss of factories

* fix(sso): needs a haircut but she works

* fix(sso): init rest w function, not side-effects

* fix(sso): /authn => /sso

* chore(sso): errors

* chore(sso): test test test

* chore(sso): test all the corners

---------

Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com>
Co-authored-by: Mike Tasset <mike.tasset@gmail.com>
2024-10-31 13:20:53 +01:00

89 lines
2.5 KiB
TypeScript

/* eslint-disable camelcase */
import { BaseError } from '@/modules/shared/errors'
import {
OidcProvider,
OidcProviderAttributes
} from '@/modules/workspaces/domain/sso/types'
import { generators, Issuer, type Client } from 'openid-client'
/**
* Generate the url used to direct users to the SSO provider for authorization.
* (i.e. the sign in form page for the given SSO provider)
*/
export const getProviderAuthorizationUrl = async ({
provider,
redirectUrl,
codeVerifier
}: {
provider: OidcProvider
redirectUrl: URL
codeVerifier: string
}): Promise<URL> => {
const { client } = await initializeIssuerAndClient({ provider, redirectUrl })
const code_challenge = generators.codeChallenge(codeVerifier)
return new URL(
client.authorizationUrl({
scope: 'openid email profile',
redirect_uri: redirectUrl.toString(),
code_challenge,
code_challenge_method: 'S256'
})
)
}
export const initializeIssuerAndClient = async ({
provider,
redirectUrl
}: {
provider: OidcProvider
redirectUrl?: URL
}): Promise<{ issuer: Issuer; client: Client }> => {
const issuer = await Issuer.discover(provider.issuerUrl)
const client = new issuer.Client({
client_id: provider.clientId,
client_secret: provider.clientSecret,
redirect_uris: redirectUrl ? [redirectUrl.toString()] : [],
response_types: ['code']
})
return { issuer, client }
}
export const getOIDCProviderAttributes = async ({
provider
}: {
provider: OidcProvider
}): Promise<OidcProviderAttributes> => {
try {
const { issuer, client } = await initializeIssuerAndClient({ provider })
return {
issuer: {
claimsSupported: (issuer.claims_supported as string[] | undefined) ?? [],
grantTypesSupported:
(issuer.grant_types_supported as string[] | undefined) ?? [],
responseTypesSupported:
(issuer.response_types_supported as string[] | undefined) ?? []
},
client: {
grantTypes: (client.grant_types as string[] | undefined) ?? []
}
}
} catch (err) {
if (err instanceof Error) {
if ('code' in err) {
if (err.code === 'ECONNREFUSED')
throw new BaseError(
'cannot connect to the provider, pls check the connection url',
err
)
} else if ('error' in err) {
if (err.error === 'Realm does not exist')
throw new BaseError(
"The realm doesn't exist, please check your url and OIDC config",
err
)
}
}
throw err
}
}