diff --git a/packages/client/Atmosphere.ts b/packages/client/Atmosphere.ts index 6d34782ae72..1e1f1c5e399 100644 --- a/packages/client/Atmosphere.ts +++ b/packages/client/Atmosphere.ts @@ -12,6 +12,7 @@ import { ConcreteRequest, Environment, FetchFunction, + FetchQueryFetchPolicy, GraphQLResponse, GraphQLTaggedNode, Network, @@ -340,13 +341,22 @@ export default class Atmosphere extends Environment { fetchQuery = async ( taggedNode: GraphQLTaggedNode, - variables: Variables = {} + variables: Variables = {}, + cacheConfig?: { + networkCacheConfig?: CacheConfig + fetchPolicy?: FetchQueryFetchPolicy + } ) => { let res: T['response'] try { - res = await fetchQuery(this, taggedNode, variables, { - fetchPolicy: 'store-or-network' - }).toPromise() + res = await fetchQuery( + this, + taggedNode, + variables, + cacheConfig ?? { + fetchPolicy: 'store-or-network' + } + ).toPromise() } catch (e) { return null } diff --git a/packages/client/components/EmailPasswordAuthForm.tsx b/packages/client/components/EmailPasswordAuthForm.tsx index 7c64424bff9..65c2a7d1728 100644 --- a/packages/client/components/EmailPasswordAuthForm.tsx +++ b/packages/client/components/EmailPasswordAuthForm.tsx @@ -97,8 +97,7 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => { const signInWithSSOOnly = isSSOAuthEnabled && !isInternalAuthEnabled const [isSSO, setIsSSO] = useState(isSSODefault || signInWithSSOOnly) const [pendingDomain, setPendingDomain] = useState('') - const [ssoURL, setSSOURL] = useState('') - const [ssoDomain, setSSODomain] = useState('') + const [ssoDomain, setSSODomain] = useState() const {submitMutation, onCompleted, submitting, error, onError} = useMutationProps() const atmosphere = useAtmosphere() const {history} = useRouter() @@ -131,9 +130,10 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => { const domain = getSSODomainFromEmail(email) if (domain && domain !== pendingDomain) { setPendingDomain(domain) + // Fetch the url to verify SSO is configured for this domain. + // Don't cache it as we need a fresh one for login const url = await getSSOUrl(atmosphere, email) - setSSODomain(domain) - setSSOURL(url || '') + setSSODomain(url ? domain : undefined) } } } @@ -148,8 +148,8 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => { const tryLoginWithSSO = async (email: string) => { const domain = getSSODomainFromEmail(email)! - const validSSOURL = domain === ssoDomain && ssoURL - const isProbablySSO = isSSO || !fields.password.value || validSSOURL + const hadValidSSOURL = domain === ssoDomain + const isProbablySSO = isSSO || !fields.password.value || hadValidSSOURL const top = getOffsetTop?.() || 56 let optimisticPopup if (isProbablySSO) { @@ -168,7 +168,7 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => { getOAuthPopupFeatures({width: 385, height: 576, top}) ) } - const url = validSSOURL || (await getSSOUrl(atmosphere, email)) + const url = await getSSOUrl(atmosphere, email) if (!url) { optimisticPopup?.close() return false diff --git a/packages/client/utils/getSAMLIdP.ts b/packages/client/utils/getSAMLIdP.ts index 6d3924453c3..aed822d7dda 100644 --- a/packages/client/utils/getSAMLIdP.ts +++ b/packages/client/utils/getSAMLIdP.ts @@ -9,7 +9,9 @@ const query = graphql` ` const getSAMLIdP = async (atmosphere: Atmosphere, variables: getSAMLIdPQuery['variables']) => { - const res = await atmosphere.fetchQuery(query, variables) + const res = await atmosphere.fetchQuery(query, variables, { + fetchPolicy: 'network-only' + }) return res?.SAMLIdP ?? null } diff --git a/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts b/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts index 86d5fcc0a4b..a1148d6669f 100644 --- a/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts +++ b/packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts @@ -38,7 +38,7 @@ const resolveDowngradeToStarter = async ( .execute(), pg .updateTable('SAML') - .set({metadata: null, url: null, lastUpdatedBy: user.id}) + .set({metadata: null, metadataURL: null, lastUpdatedBy: user.id}) .where('orgId', '=', orgId) .execute(), updateTeamByOrgId( diff --git a/packages/server/graphql/private/mutations/loginSAML.ts b/packages/server/graphql/private/mutations/loginSAML.ts index 9e861aa4f98..a5bdb5bfbdd 100644 --- a/packages/server/graphql/private/mutations/loginSAML.ts +++ b/packages/server/graphql/private/mutations/loginSAML.ts @@ -115,13 +115,14 @@ const loginSAML: MutationResolvers['loginSAML'] = async ( if (newMetadata) { // The user is updating their SAML metadata // Revalidate it & persist to DB + // Generate the URL to verify the metadata, don't persist it as it needs to be generated fresh const url = getSignOnURL(metadata, normalizedName) if (url instanceof Error) { return standardError(url) } await pg .updateTable('SAML') - .set({metadata: newMetadata, metadataURL: newMetadataURL, url}) + .set({metadata: newMetadata, metadataURL: newMetadataURL}) .where('id', '=', normalizedName) .execute() } diff --git a/packages/server/postgres/migrations/2024-12-12T08:40:19.695Z_removeURLFromSAML.ts b/packages/server/postgres/migrations/2024-12-12T08:40:19.695Z_removeURLFromSAML.ts new file mode 100644 index 00000000000..8a8cd5fc15b --- /dev/null +++ b/packages/server/postgres/migrations/2024-12-12T08:40:19.695Z_removeURLFromSAML.ts @@ -0,0 +1,9 @@ +import type {Kysely} from 'kysely' + +export async function up(db: Kysely): Promise { + await db.schema.alterTable('SAML').dropColumn('url').execute() +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('SAML').addColumn('url', 'varchar(2056)').execute() +} diff --git a/packages/server/utils/getSAMLURLFromEmail.ts b/packages/server/utils/getSAMLURLFromEmail.ts index feb6d9a3075..ea16992fb18 100644 --- a/packages/server/utils/getSAMLURLFromEmail.ts +++ b/packages/server/utils/getSAMLURLFromEmail.ts @@ -2,6 +2,7 @@ import base64url from 'base64url' import getSSODomainFromEmail from 'parabol-client/utils/getSSODomainFromEmail' import {URL} from 'url' import {DataLoaderWorker} from '../graphql/graphql' +import getSignOnURL from '../graphql/public/mutations/helpers/SAMLHelpers/getSignOnURL' import getKysely from '../postgres/getKysely' export const isSingleTenantSSO = @@ -26,14 +27,17 @@ const getSAMLURLFromEmail = async ( if (isSingleTenantSSO) { // For PPMI use const pg = getKysely() - const instanceURLres = await pg + const instanceRes = await pg .selectFrom('SAML') - .select('url') - .where('url', 'is not', null) + .select(['id', 'metadata']) + .where('metadata', 'is not', null) .limit(1) .executeTakeFirst() - const instanceURL = instanceURLres?.url - if (!instanceURL) return null + if (!instanceRes) return null + const {id, metadata} = instanceRes + if (!metadata) return null + const instanceURL = getSignOnURL(metadata, id) + if (instanceURL instanceof Error) return null return urlWithRelayState(instanceURL, isInvited) } if (!email) return null @@ -42,8 +46,10 @@ const getSAMLURLFromEmail = async ( const saml = await dataLoader.get('samlByDomain').load(domainName) if (!saml) return null - const {url} = saml - if (!url) return null + const {id, metadata} = saml + if (!metadata) return null + const url = getSignOnURL(metadata, id) + if (url instanceof Error) return null return urlWithRelayState(url, isInvited) }