Skip to content

Commit 22d89e5

Browse files
authored
fix: Refresh the SAML request URL for each login attempt (#10593)
1 parent dbe20eb commit 22d89e5

File tree

7 files changed

+49
-21
lines changed

7 files changed

+49
-21
lines changed

packages/client/Atmosphere.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ConcreteRequest,
1313
Environment,
1414
FetchFunction,
15+
FetchQueryFetchPolicy,
1516
GraphQLResponse,
1617
GraphQLTaggedNode,
1718
Network,
@@ -340,13 +341,22 @@ export default class Atmosphere extends Environment {
340341

341342
fetchQuery = async <T extends OperationType>(
342343
taggedNode: GraphQLTaggedNode,
343-
variables: Variables = {}
344+
variables: Variables = {},
345+
cacheConfig?: {
346+
networkCacheConfig?: CacheConfig
347+
fetchPolicy?: FetchQueryFetchPolicy
348+
}
344349
) => {
345350
let res: T['response']
346351
try {
347-
res = await fetchQuery<T>(this, taggedNode, variables, {
348-
fetchPolicy: 'store-or-network'
349-
}).toPromise()
352+
res = await fetchQuery<T>(
353+
this,
354+
taggedNode,
355+
variables,
356+
cacheConfig ?? {
357+
fetchPolicy: 'store-or-network'
358+
}
359+
).toPromise()
350360
} catch (e) {
351361
return null
352362
}

packages/client/components/EmailPasswordAuthForm.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => {
9797
const signInWithSSOOnly = isSSOAuthEnabled && !isInternalAuthEnabled
9898
const [isSSO, setIsSSO] = useState(isSSODefault || signInWithSSOOnly)
9999
const [pendingDomain, setPendingDomain] = useState('')
100-
const [ssoURL, setSSOURL] = useState('')
101-
const [ssoDomain, setSSODomain] = useState('')
100+
const [ssoDomain, setSSODomain] = useState<string>()
102101
const {submitMutation, onCompleted, submitting, error, onError} = useMutationProps()
103102
const atmosphere = useAtmosphere()
104103
const {history} = useRouter()
@@ -131,9 +130,10 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => {
131130
const domain = getSSODomainFromEmail(email)
132131
if (domain && domain !== pendingDomain) {
133132
setPendingDomain(domain)
133+
// Fetch the url to verify SSO is configured for this domain.
134+
// Don't cache it as we need a fresh one for login
134135
const url = await getSSOUrl(atmosphere, email)
135-
setSSODomain(domain)
136-
setSSOURL(url || '')
136+
setSSODomain(url ? domain : undefined)
137137
}
138138
}
139139
}
@@ -148,8 +148,8 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => {
148148

149149
const tryLoginWithSSO = async (email: string) => {
150150
const domain = getSSODomainFromEmail(email)!
151-
const validSSOURL = domain === ssoDomain && ssoURL
152-
const isProbablySSO = isSSO || !fields.password.value || validSSOURL
151+
const hadValidSSOURL = domain === ssoDomain
152+
const isProbablySSO = isSSO || !fields.password.value || hadValidSSOURL
153153
const top = getOffsetTop?.() || 56
154154
let optimisticPopup
155155
if (isProbablySSO) {
@@ -168,7 +168,7 @@ const EmailPasswordAuthForm = forwardRef((props: Props, ref: any) => {
168168
getOAuthPopupFeatures({width: 385, height: 576, top})
169169
)
170170
}
171-
const url = validSSOURL || (await getSSOUrl(atmosphere, email))
171+
const url = await getSSOUrl(atmosphere, email)
172172
if (!url) {
173173
optimisticPopup?.close()
174174
return false

packages/client/utils/getSAMLIdP.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ const query = graphql`
99
`
1010

1111
const getSAMLIdP = async (atmosphere: Atmosphere, variables: getSAMLIdPQuery['variables']) => {
12-
const res = await atmosphere.fetchQuery<getSAMLIdPQuery>(query, variables)
12+
const res = await atmosphere.fetchQuery<getSAMLIdPQuery>(query, variables, {
13+
fetchPolicy: 'network-only'
14+
})
1315
return res?.SAMLIdP ?? null
1416
}
1517

packages/server/graphql/mutations/helpers/resolveDowngradeToStarter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const resolveDowngradeToStarter = async (
3838
.execute(),
3939
pg
4040
.updateTable('SAML')
41-
.set({metadata: null, url: null, lastUpdatedBy: user.id})
41+
.set({metadata: null, metadataURL: null, lastUpdatedBy: user.id})
4242
.where('orgId', '=', orgId)
4343
.execute(),
4444
updateTeamByOrgId(

packages/server/graphql/private/mutations/loginSAML.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,14 @@ const loginSAML: MutationResolvers['loginSAML'] = async (
115115
if (newMetadata) {
116116
// The user is updating their SAML metadata
117117
// Revalidate it & persist to DB
118+
// Generate the URL to verify the metadata, don't persist it as it needs to be generated fresh
118119
const url = getSignOnURL(metadata, normalizedName)
119120
if (url instanceof Error) {
120121
return standardError(url)
121122
}
122123
await pg
123124
.updateTable('SAML')
124-
.set({metadata: newMetadata, metadataURL: newMetadataURL, url})
125+
.set({metadata: newMetadata, metadataURL: newMetadataURL})
125126
.where('id', '=', normalizedName)
126127
.execute()
127128
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type {Kysely} from 'kysely'
2+
3+
export async function up(db: Kysely<any>): Promise<void> {
4+
await db.schema.alterTable('SAML').dropColumn('url').execute()
5+
}
6+
7+
export async function down(db: Kysely<any>): Promise<void> {
8+
await db.schema.alterTable('SAML').addColumn('url', 'varchar(2056)').execute()
9+
}

packages/server/utils/getSAMLURLFromEmail.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import base64url from 'base64url'
22
import getSSODomainFromEmail from 'parabol-client/utils/getSSODomainFromEmail'
33
import {URL} from 'url'
44
import {DataLoaderWorker} from '../graphql/graphql'
5+
import getSignOnURL from '../graphql/public/mutations/helpers/SAMLHelpers/getSignOnURL'
56
import getKysely from '../postgres/getKysely'
67

78
export const isSingleTenantSSO =
@@ -26,14 +27,17 @@ const getSAMLURLFromEmail = async (
2627
if (isSingleTenantSSO) {
2728
// For PPMI use
2829
const pg = getKysely()
29-
const instanceURLres = await pg
30+
const instanceRes = await pg
3031
.selectFrom('SAML')
31-
.select('url')
32-
.where('url', 'is not', null)
32+
.select(['id', 'metadata'])
33+
.where('metadata', 'is not', null)
3334
.limit(1)
3435
.executeTakeFirst()
35-
const instanceURL = instanceURLres?.url
36-
if (!instanceURL) return null
36+
if (!instanceRes) return null
37+
const {id, metadata} = instanceRes
38+
if (!metadata) return null
39+
const instanceURL = getSignOnURL(metadata, id)
40+
if (instanceURL instanceof Error) return null
3741
return urlWithRelayState(instanceURL, isInvited)
3842
}
3943
if (!email) return null
@@ -42,8 +46,10 @@ const getSAMLURLFromEmail = async (
4246

4347
const saml = await dataLoader.get('samlByDomain').load(domainName)
4448
if (!saml) return null
45-
const {url} = saml
46-
if (!url) return null
49+
const {id, metadata} = saml
50+
if (!metadata) return null
51+
const url = getSignOnURL(metadata, id)
52+
if (url instanceof Error) return null
4753
return urlWithRelayState(url, isInvited)
4854
}
4955

0 commit comments

Comments
 (0)