From bcfc1cfe568401cc73aebf8616b80554245e9a41 Mon Sep 17 00:00:00 2001 From: Shalabh Agarwal Date: Sat, 28 Feb 2026 01:08:05 +0530 Subject: [PATCH 1/5] sso changes --- .../2026.03.02T00.00.00.add-oidc-sso.sql | 23 + client/src/components/Sidebar.tsx | 2 +- client/src/graphql/generated.ts | 320 +++++++++-- client/src/webpages/App.tsx | 8 + client/src/webpages/auth/LoginSSO.tsx | 11 +- client/src/webpages/auth/SignUp.tsx | 22 +- client/src/webpages/dashboard/Dashboard.tsx | 5 + client/src/webpages/settings/SSOSettings.tsx | 538 +++++++++++++----- server/.env.example | 2 + server/api.ts | 149 ++++- server/graphql/generated.ts | 103 +++- server/graphql/modules/authentication.ts | 7 +- server/graphql/modules/org.ts | 167 +++++- server/graphql/schema.ts | 2 + server/package-lock.json | 32 ++ server/package.json | 1 + server/services/SSOService/SSOService.ts | 28 +- server/services/SSOService/dbTypes.ts | 2 +- .../orgSettingsService/orgSettingsService.ts | 82 ++- .../services/userManagementService/dbTypes.ts | 2 +- .../userManagementService.ts | 3 +- 21 files changed, 1283 insertions(+), 226 deletions(-) create mode 100644 .devops/migrator/src/scripts/api-server-pg/2026.03.02T00.00.00.add-oidc-sso.sql diff --git a/.devops/migrator/src/scripts/api-server-pg/2026.03.02T00.00.00.add-oidc-sso.sql b/.devops/migrator/src/scripts/api-server-pg/2026.03.02T00.00.00.add-oidc-sso.sql new file mode 100644 index 00000000..99ae0b42 --- /dev/null +++ b/.devops/migrator/src/scripts/api-server-pg/2026.03.02T00.00.00.add-oidc-sso.sql @@ -0,0 +1,23 @@ + -- Add OIDC columns +ALTER TABLE public.org_settings +ADD COLUMN IF NOT EXISTS oidc_enabled boolean DEFAULT false NOT NULL, +ADD COLUMN IF NOT EXISTS issuer_url character varying(255), +ADD COLUMN IF NOT EXISTS client_id character varying(255), +ADD COLUMN IF NOT EXISTS client_secret character varying(255); + +-- Add OIDC settings constraint +ALTER TABLE public.org_settings +ADD CONSTRAINT oidc_settings_constraint CHECK ( + (oidc_enabled = false) OR + ((oidc_enabled = true) AND (client_id IS NOT NULL) AND (client_secret IS NOT NULL) AND (issuer_url IS NOT NULL)) +); + +-- Add mutual exclusivity: SAML and OIDC cannot both be enabled +ALTER TABLE public.org_settings +ADD CONSTRAINT sso_saml_oidc_constraint CHECK ( + ((saml_enabled = false) AND (oidc_enabled = true)) OR + ((saml_enabled = true) AND (oidc_enabled = false)) OR + ((saml_enabled = false) AND (oidc_enabled = false)) +); + +ALTER TYPE public.login_method_enum ADD VALUE IF NOT EXISTS 'oidc'; \ No newline at end of file diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index aa4d6b91..a0be719f 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -45,7 +45,7 @@ const MenuItemNames = makeEnumLike([ 'Users', 'Employee Safety', 'NCMEC Settings', - 'SSO', + 'SSO Settings', 'Organization', ]); diff --git a/client/src/graphql/generated.ts b/client/src/graphql/generated.ts index d9b7e1b3..9491fc8e 100644 --- a/client/src/graphql/generated.ts +++ b/client/src/graphql/generated.ts @@ -1399,6 +1399,7 @@ export type GQLInviteUserInput = { export type GQLInviteUserToken = { readonly __typename: 'InviteUserToken'; readonly email: Scalars['String']; + readonly oidcEnabled: Scalars['Boolean']; readonly orgId: Scalars['String']; readonly role: GQLUserRole; readonly samlEnabled: Scalars['Boolean']; @@ -1935,6 +1936,7 @@ export type GQLLoginInput = { }; export const GQLLoginMethod = { + Oidc: 'OIDC', Password: 'PASSWORD', Saml: 'SAML', } as const; @@ -2368,6 +2370,7 @@ export type GQLMutation = { readonly setPluginIntegrationConfig: GQLSetIntegrationConfigResponse; readonly signUp: GQLSignUpResponse; readonly submitManualReviewDecision: GQLSubmitDecisionResponse; + readonly switchSSOMethod: GQLOrg; readonly updateAccountInfo?: Maybe; readonly updateAction: GQLMutateActionResponse; readonly updateAppealSettings: GQLAppealSettings; @@ -2383,7 +2386,8 @@ export type GQLMutation = { readonly updateReportingRule: GQLUpdateReportingRuleResponse; readonly updateRole?: Maybe; readonly updateRoutingRule: GQLUpdateRoutingRuleResponse; - readonly updateSSOCredentials: Scalars['Boolean']; + readonly updateSSOOidcCredentials: Scalars['Boolean']; + readonly updateSSOSamlCredentials: Scalars['Boolean']; readonly updateTextBank: GQLMutateBankResponse; readonly updateThreadItemType: GQLMutateThreadItemTypeResponse; readonly updateUserItemType: GQLMutateUserItemTypeResponse; @@ -2631,6 +2635,10 @@ export type GQLMutationSubmitManualReviewDecisionArgs = { input: GQLSubmitDecisionInput; }; +export type GQLMutationSwitchSsoMethodArgs = { + input: GQLSwitchSsoMethodInput; +}; + export type GQLMutationUpdateAccountInfoArgs = { firstName?: InputMaybe; lastName?: InputMaybe; @@ -2693,8 +2701,12 @@ export type GQLMutationUpdateRoutingRuleArgs = { input: GQLUpdateRoutingRuleInput; }; -export type GQLMutationUpdateSsoCredentialsArgs = { - input: GQLUpdateSsoCredentialsInput; +export type GQLMutationUpdateSsoOidcCredentialsArgs = { + input: GQLUpdateSsoOidcCredentialsInput; +}; + +export type GQLMutationUpdateSsoSamlCredentialsArgs = { + input: GQLUpdateSsoSamlCredentialsInput; }; export type GQLMutationUpdateTextBankArgs = { @@ -2945,6 +2957,8 @@ export type GQLOrg = { readonly apiKey: Scalars['String']; readonly appealsRoutingRules: ReadonlyArray; readonly banks?: Maybe; + readonly clientId?: Maybe; + readonly clientSecret?: Maybe; readonly contentTypes: ReadonlyArray; readonly defaultInterfacePreferences: GQLUserInterfacePreferences; readonly email: Scalars['String']; @@ -2956,10 +2970,13 @@ export type GQLOrg = { readonly id: Scalars['ID']; readonly integrationConfigs: ReadonlyArray; readonly isDemoOrg: Scalars['Boolean']; + readonly issuerUrl?: Maybe; readonly itemTypes: ReadonlyArray; readonly mrtQueues: ReadonlyArray; readonly name: Scalars['String']; readonly ncmecReports: ReadonlyArray; + readonly oidcCallbackUrl?: Maybe; + readonly oidcEnabled: Scalars['Boolean']; readonly onCallAlertEmail?: Maybe; readonly pendingInvites: ReadonlyArray; readonly policies: ReadonlyArray; @@ -2970,6 +2987,7 @@ export type GQLOrg = { readonly requiresPolicyForDecisionsInMrt: Scalars['Boolean']; readonly routingRules: ReadonlyArray; readonly rules: ReadonlyArray; + readonly samlEnabled: Scalars['Boolean']; readonly signals: ReadonlyArray; readonly ssoCert?: Maybe; readonly ssoUrl?: Maybe; @@ -3165,6 +3183,7 @@ export type GQLQuery = { readonly getRecentDecisions: ReadonlyArray; readonly getResolvedJobCounts: ReadonlyArray; readonly getResolvedJobsForUser: Scalars['Int']; + readonly getSSOOidcCallbackUrl?: Maybe; readonly getSSORedirectUrl?: Maybe; readonly getSkippedJobCounts: ReadonlyArray; readonly getSkippedJobsForUser: Scalars['Int']; @@ -3910,6 +3929,12 @@ export type GQLRunRetroactionSuccessResponse = { readonly _?: Maybe; }; +export const GQLSsoMethod = { + Oidc: 'OIDC', + Saml: 'SAML', +} as const; + +export type GQLSsoMethod = (typeof GQLSsoMethod)[keyof typeof GQLSsoMethod]; export type GQLScalarSignalOutputType = { readonly __typename: 'ScalarSignalOutputType'; readonly scalarType: GQLScalarType; @@ -4242,6 +4267,15 @@ export type GQLSubmittedJobActionNotFoundError = GQLError & { export type GQLSupportedLanguages = GQLAllLanguages | GQLLanguages; +export type GQLSwitchSsoMethodInput = { + readonly clientId?: InputMaybe; + readonly clientSecret?: InputMaybe; + readonly issuerUrl?: InputMaybe; + readonly method: GQLSsoMethod; + readonly ssoCert?: InputMaybe; + readonly ssoUrl?: InputMaybe; +}; + export type GQLTableDecisionCount = { readonly __typename: 'TableDecisionCount'; readonly action_id?: Maybe; @@ -4530,7 +4564,15 @@ export type GQLUpdateRoutingRuleResponse = | GQLQueueDoesNotExistError | GQLRoutingRuleNameExistsError; -export type GQLUpdateSsoCredentialsInput = { +export type GQLUpdateSsoOidcCredentialsInput = { + readonly clientId: Scalars['String']; + readonly clientSecret: Scalars['String']; + readonly issuerUrl: Scalars['String']; + readonly oidcEnabled: Scalars['Boolean']; +}; + +export type GQLUpdateSsoSamlCredentialsInput = { + readonly samlEnabled: Scalars['Boolean']; readonly ssoCert: Scalars['String']; readonly ssoUrl: Scalars['String']; }; @@ -5234,6 +5276,7 @@ export type GQLInviteUserTokenQuery = { readonly role: GQLUserRole; readonly orgId: string; readonly samlEnabled: boolean; + readonly oidcEnabled: boolean; }; }; }; @@ -24100,18 +24143,60 @@ export type GQLGetSsoCredentialsQuery = { readonly myOrg?: { readonly __typename: 'Org'; readonly id: string; + readonly samlEnabled: boolean; + readonly oidcEnabled: boolean; readonly ssoUrl?: string | null; readonly ssoCert?: string | null; + readonly issuerUrl?: string | null; + readonly clientId?: string | null; + readonly clientSecret?: string | null; } | null; }; -export type GQLUpdateSsoCredentialsMutationVariables = Exact<{ - input: GQLUpdateSsoCredentialsInput; +export type GQLGetSsoOidcCallbackUrlQueryVariables = Exact<{ + [key: string]: never; +}>; + +export type GQLGetSsoOidcCallbackUrlQuery = { + readonly __typename: 'Query'; + readonly getSSOOidcCallbackUrl?: string | null; +}; + +export type GQLUpdateSsoSamlCredentialsMutationVariables = Exact<{ + input: GQLUpdateSsoSamlCredentialsInput; }>; -export type GQLUpdateSsoCredentialsMutation = { +export type GQLUpdateSsoSamlCredentialsMutation = { readonly __typename: 'Mutation'; - readonly updateSSOCredentials: boolean; + readonly updateSSOSamlCredentials: boolean; +}; + +export type GQLUpdateSsoOidcCredentialsMutationVariables = Exact<{ + input: GQLUpdateSsoOidcCredentialsInput; +}>; + +export type GQLUpdateSsoOidcCredentialsMutation = { + readonly __typename: 'Mutation'; + readonly updateSSOOidcCredentials: boolean; +}; + +export type GQLSwitchSsoMethodMutationVariables = Exact<{ + input: GQLSwitchSsoMethodInput; +}>; + +export type GQLSwitchSsoMethodMutation = { + readonly __typename: 'Mutation'; + readonly switchSSOMethod: { + readonly __typename: 'Org'; + readonly id: string; + readonly samlEnabled: boolean; + readonly oidcEnabled: boolean; + readonly ssoUrl?: string | null; + readonly ssoCert?: string | null; + readonly issuerUrl?: string | null; + readonly clientId?: string | null; + readonly clientSecret?: string | null; + }; }; export const GQLCustomActionFragmentFragmentDoc = gql` @@ -26188,6 +26273,7 @@ export const GQLInviteUserTokenDocument = gql` role orgId samlEnabled + oidcEnabled } } ... on InviteUserTokenExpiredError { @@ -37771,8 +37857,13 @@ export const GQLGetSsoCredentialsDocument = gql` } myOrg { id + samlEnabled + oidcEnabled ssoUrl ssoCert + issuerUrl + clientId + clientSecret } } `; @@ -37826,53 +37917,215 @@ export type GQLGetSsoCredentialsQueryResult = Apollo.QueryResult< GQLGetSsoCredentialsQuery, GQLGetSsoCredentialsQueryVariables >; -export const GQLUpdateSsoCredentialsDocument = gql` - mutation UpdateSSOCredentials($input: UpdateSSOCredentialsInput!) { - updateSSOCredentials(input: $input) +export const GQLGetSsoOidcCallbackUrlDocument = gql` + query GetSSOOidcCallbackUrl { + getSSOOidcCallbackUrl + } +`; + +/** + * __useGQLGetSsoOidcCallbackUrlQuery__ + * + * To run a query within a React component, call `useGQLGetSsoOidcCallbackUrlQuery` and pass it any options that fit your needs. + * When your component renders, `useGQLGetSsoOidcCallbackUrlQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGQLGetSsoOidcCallbackUrlQuery({ + * variables: { + * }, + * }); + */ +export function useGQLGetSsoOidcCallbackUrlQuery( + baseOptions?: Apollo.QueryHookOptions< + GQLGetSsoOidcCallbackUrlQuery, + GQLGetSsoOidcCallbackUrlQueryVariables + >, +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useQuery< + GQLGetSsoOidcCallbackUrlQuery, + GQLGetSsoOidcCallbackUrlQueryVariables + >(GQLGetSsoOidcCallbackUrlDocument, options); +} +export function useGQLGetSsoOidcCallbackUrlLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions< + GQLGetSsoOidcCallbackUrlQuery, + GQLGetSsoOidcCallbackUrlQueryVariables + >, +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery< + GQLGetSsoOidcCallbackUrlQuery, + GQLGetSsoOidcCallbackUrlQueryVariables + >(GQLGetSsoOidcCallbackUrlDocument, options); +} +export type GQLGetSsoOidcCallbackUrlQueryHookResult = ReturnType< + typeof useGQLGetSsoOidcCallbackUrlQuery +>; +export type GQLGetSsoOidcCallbackUrlLazyQueryHookResult = ReturnType< + typeof useGQLGetSsoOidcCallbackUrlLazyQuery +>; +export type GQLGetSsoOidcCallbackUrlQueryResult = Apollo.QueryResult< + GQLGetSsoOidcCallbackUrlQuery, + GQLGetSsoOidcCallbackUrlQueryVariables +>; +export const GQLUpdateSsoSamlCredentialsDocument = gql` + mutation UpdateSSOSamlCredentials($input: UpdateSSOSamlCredentialsInput!) { + updateSSOSamlCredentials(input: $input) + } +`; +export type GQLUpdateSsoSamlCredentialsMutationFn = Apollo.MutationFunction< + GQLUpdateSsoSamlCredentialsMutation, + GQLUpdateSsoSamlCredentialsMutationVariables +>; + +/** + * __useGQLUpdateSsoSamlCredentialsMutation__ + * + * To run a mutation, you first call `useGQLUpdateSsoSamlCredentialsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGQLUpdateSsoSamlCredentialsMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [gqlUpdateSsoSamlCredentialsMutation, { data, loading, error }] = useGQLUpdateSsoSamlCredentialsMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useGQLUpdateSsoSamlCredentialsMutation( + baseOptions?: Apollo.MutationHookOptions< + GQLUpdateSsoSamlCredentialsMutation, + GQLUpdateSsoSamlCredentialsMutationVariables + >, +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + GQLUpdateSsoSamlCredentialsMutation, + GQLUpdateSsoSamlCredentialsMutationVariables + >(GQLUpdateSsoSamlCredentialsDocument, options); +} +export type GQLUpdateSsoSamlCredentialsMutationHookResult = ReturnType< + typeof useGQLUpdateSsoSamlCredentialsMutation +>; +export type GQLUpdateSsoSamlCredentialsMutationResult = + Apollo.MutationResult; +export type GQLUpdateSsoSamlCredentialsMutationOptions = + Apollo.BaseMutationOptions< + GQLUpdateSsoSamlCredentialsMutation, + GQLUpdateSsoSamlCredentialsMutationVariables + >; +export const GQLUpdateSsoOidcCredentialsDocument = gql` + mutation UpdateSSOOidcCredentials($input: UpdateSSOOidcCredentialsInput!) { + updateSSOOidcCredentials(input: $input) + } +`; +export type GQLUpdateSsoOidcCredentialsMutationFn = Apollo.MutationFunction< + GQLUpdateSsoOidcCredentialsMutation, + GQLUpdateSsoOidcCredentialsMutationVariables +>; + +/** + * __useGQLUpdateSsoOidcCredentialsMutation__ + * + * To run a mutation, you first call `useGQLUpdateSsoOidcCredentialsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGQLUpdateSsoOidcCredentialsMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [gqlUpdateSsoOidcCredentialsMutation, { data, loading, error }] = useGQLUpdateSsoOidcCredentialsMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useGQLUpdateSsoOidcCredentialsMutation( + baseOptions?: Apollo.MutationHookOptions< + GQLUpdateSsoOidcCredentialsMutation, + GQLUpdateSsoOidcCredentialsMutationVariables + >, +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + GQLUpdateSsoOidcCredentialsMutation, + GQLUpdateSsoOidcCredentialsMutationVariables + >(GQLUpdateSsoOidcCredentialsDocument, options); +} +export type GQLUpdateSsoOidcCredentialsMutationHookResult = ReturnType< + typeof useGQLUpdateSsoOidcCredentialsMutation +>; +export type GQLUpdateSsoOidcCredentialsMutationResult = + Apollo.MutationResult; +export type GQLUpdateSsoOidcCredentialsMutationOptions = + Apollo.BaseMutationOptions< + GQLUpdateSsoOidcCredentialsMutation, + GQLUpdateSsoOidcCredentialsMutationVariables + >; +export const GQLSwitchSsoMethodDocument = gql` + mutation SwitchSSOMethod($input: SwitchSSOMethodInput!) { + switchSSOMethod(input: $input) { + id + samlEnabled + oidcEnabled + ssoUrl + ssoCert + issuerUrl + clientId + clientSecret + } } `; -export type GQLUpdateSsoCredentialsMutationFn = Apollo.MutationFunction< - GQLUpdateSsoCredentialsMutation, - GQLUpdateSsoCredentialsMutationVariables +export type GQLSwitchSsoMethodMutationFn = Apollo.MutationFunction< + GQLSwitchSsoMethodMutation, + GQLSwitchSsoMethodMutationVariables >; /** - * __useGQLUpdateSsoCredentialsMutation__ + * __useGQLSwitchSsoMethodMutation__ * - * To run a mutation, you first call `useGQLUpdateSsoCredentialsMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useGQLUpdateSsoCredentialsMutation` returns a tuple that includes: + * To run a mutation, you first call `useGQLSwitchSsoMethodMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGQLSwitchSsoMethodMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example - * const [gqlUpdateSsoCredentialsMutation, { data, loading, error }] = useGQLUpdateSsoCredentialsMutation({ + * const [gqlSwitchSsoMethodMutation, { data, loading, error }] = useGQLSwitchSsoMethodMutation({ * variables: { * input: // value for 'input' * }, * }); */ -export function useGQLUpdateSsoCredentialsMutation( +export function useGQLSwitchSsoMethodMutation( baseOptions?: Apollo.MutationHookOptions< - GQLUpdateSsoCredentialsMutation, - GQLUpdateSsoCredentialsMutationVariables + GQLSwitchSsoMethodMutation, + GQLSwitchSsoMethodMutationVariables >, ) { const options = { ...defaultOptions, ...baseOptions }; return Apollo.useMutation< - GQLUpdateSsoCredentialsMutation, - GQLUpdateSsoCredentialsMutationVariables - >(GQLUpdateSsoCredentialsDocument, options); + GQLSwitchSsoMethodMutation, + GQLSwitchSsoMethodMutationVariables + >(GQLSwitchSsoMethodDocument, options); } -export type GQLUpdateSsoCredentialsMutationHookResult = ReturnType< - typeof useGQLUpdateSsoCredentialsMutation +export type GQLSwitchSsoMethodMutationHookResult = ReturnType< + typeof useGQLSwitchSsoMethodMutation >; -export type GQLUpdateSsoCredentialsMutationResult = - Apollo.MutationResult; -export type GQLUpdateSsoCredentialsMutationOptions = Apollo.BaseMutationOptions< - GQLUpdateSsoCredentialsMutation, - GQLUpdateSsoCredentialsMutationVariables +export type GQLSwitchSsoMethodMutationResult = + Apollo.MutationResult; +export type GQLSwitchSsoMethodMutationOptions = Apollo.BaseMutationOptions< + GQLSwitchSsoMethodMutation, + GQLSwitchSsoMethodMutationVariables >; export const namedOperations = { Query: { @@ -38000,6 +38253,7 @@ export const namedOperations = { OrgDefaultSafetySettings: 'OrgDefaultSafetySettings', OrgSettings: 'OrgSettings', GetSSOCredentials: 'GetSSOCredentials', + GetSSOOidcCallbackUrl: 'GetSSOOidcCallbackUrl', }, Mutation: { RotateApiKey: 'RotateApiKey', @@ -38079,7 +38333,9 @@ export const namedOperations = { UpdateNcmecOrgSettings: 'UpdateNcmecOrgSettings', SetOrgDefaultSafetySettings: 'SetOrgDefaultSafetySettings', UpdateOrgInfo: 'UpdateOrgInfo', - UpdateSSOCredentials: 'UpdateSSOCredentials', + UpdateSSOSamlCredentials: 'UpdateSSOSamlCredentials', + UpdateSSOOidcCredentials: 'UpdateSSOOidcCredentials', + SwitchSSOMethod: 'SwitchSSOMethod', }, Fragment: { CustomActionFragment: 'CustomActionFragment', diff --git a/client/src/webpages/App.tsx b/client/src/webpages/App.tsx index cc1d5472..97e56abf 100644 --- a/client/src/webpages/App.tsx +++ b/client/src/webpages/App.tsx @@ -76,6 +76,14 @@ export default function App() { // just redirect them to login. element: , }, + { + path: 'login/sso_saml', + element: ( + + + + ), + }, { path: 'login/sso', element: ( diff --git a/client/src/webpages/auth/LoginSSO.tsx b/client/src/webpages/auth/LoginSSO.tsx index 0d65b2dc..4281f86c 100644 --- a/client/src/webpages/auth/LoginSSO.tsx +++ b/client/src/webpages/auth/LoginSSO.tsx @@ -3,10 +3,12 @@ import { Label } from '@/coop-ui/Label'; import { useGQLGetSsoRedirectUrlLazyQuery } from '@/graphql/generated'; import { gql } from '@apollo/client'; import { Input } from 'antd'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link } from 'react-router-dom'; +import { toast } from '@/coop-ui/Toast'; + import CoopButton from '../dashboard/components/CoopButton'; import CoopModal from '../dashboard/components/CoopModal'; @@ -27,6 +29,13 @@ export default function LoginSSO() { const [errorModalVisible, setErrorModalVisible] = useState(false); const [getSSORedirectUrl, { loading }] = useGQLGetSsoRedirectUrlLazyQuery(); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + if (params.get('error') === 'access_denied') { + toast.error('SSO login was cancelled. Please try again.'); + } + }, []); const errorModal = ( - {!tokenInfo.samlEnabled && ( + {(!tokenInfo.samlEnabled && !tokenInfo.oidcEnabled) && (