From 04f8ece38972417fd29571f0c268cfa03c652383 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 8 Dec 2023 20:24:19 +0700 Subject: [PATCH 1/3] Load SAML auth providers from environment --- .env.example | 4 ++++ src/commons/utils/AuthHelper.ts | 6 +++++- src/commons/utils/Constants.ts | 12 ++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 13c8fa2556..75bf6f14d6 100644 --- a/.env.example +++ b/.env.example @@ -41,6 +41,10 @@ REACT_APP_CAS_PROVIDER1= REACT_APP_CAS_PROVIDER1_NAME= REACT_APP_CAS_PROVIDER1_ENDPOINT= +REACT_APP_SAML_PROVIDER1= +REACT_APP_SAML_PROVIDER1_NAME= +REACT_APP_SAML_PROVIDER1_ENDPOINT= + REACT_APP_MODULE_BACKEND_URL=https://source-academy.github.io/modules REACT_APP_SHAREDB_BACKEND_URL= REACT_APP_SICPJS_BACKEND_URL="http://127.0.0.1:8080/" diff --git a/src/commons/utils/AuthHelper.ts b/src/commons/utils/AuthHelper.ts index c8ec30669d..9c4d6c8091 100644 --- a/src/commons/utils/AuthHelper.ts +++ b/src/commons/utils/AuthHelper.ts @@ -2,7 +2,8 @@ import Constants from './Constants'; export enum AuthProviderType { OAUTH2 = 'OAUTH2', - CAS = 'CAS' + CAS = 'CAS', + SAML_SSO = 'SAML' } export function computeEndpointUrl(providerId: string): string | undefined { @@ -19,6 +20,9 @@ export function computeEndpointUrl(providerId: string): string | undefined { case AuthProviderType.CAS: epUrl.searchParams.set('service', computeRedirectUri(providerId)!); break; + case AuthProviderType.SAML_SSO: + epUrl.searchParams.set('target_url', computeRedirectUri(providerId)!); + break; } return epUrl.toString(); } catch (e) { diff --git a/src/commons/utils/Constants.ts b/src/commons/utils/Constants.ts index aff8fbcd26..58cb3ea512 100644 --- a/src/commons/utils/Constants.ts +++ b/src/commons/utils/Constants.ts @@ -77,6 +77,18 @@ for (let i = 1; ; ++i) { authProviders.set(id, { name, endpoint, isDefault: false, type: AuthProviderType.CAS }); } +for (let i = 1; ; ++i) { + const id = process.env[`REACT_APP_SAML_PROVIDER${i}`]; + if (!id) { + break; + } + + const name = process.env[`REACT_APP_SAML_PROVIDER${i}_NAME`] || 'Unnamed provider'; + const endpoint = process.env[`REACT_APP_SAML_PROVIDER${i}_ENDPOINT`] || ''; + + authProviders.set(id, { name, endpoint, isDefault: false, type: AuthProviderType.SAML_SSO }); +} + export enum Links { githubIssues = 'https://github.com/source-academy/frontend/issues', githubOrg = 'https://github.com/source-academy', From cc943fa2b1d4705b91804112cec18251e9ae37f3 Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 8 Dec 2023 20:25:36 +0700 Subject: [PATCH 2/3] Support passing of credential cookies via fetch --- src/commons/sagas/RequestsSaga.ts | 3 ++- src/commons/utils/RequestHelper.tsx | 13 +++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/commons/sagas/RequestsSaga.ts b/src/commons/sagas/RequestsSaga.ts index 3101354530..8242cce90b 100644 --- a/src/commons/sagas/RequestsSaga.ts +++ b/src/commons/sagas/RequestsSaga.ts @@ -77,7 +77,8 @@ export const postAuth = async ( ...(clientId ? { client_id: clientId } : {}), ...(redirectUri ? { redirect_uri: redirectUri } : {}) }, - errorMessage: 'Could not login. Please contact the module administrator.' + errorMessage: 'Could not login. Please contact the module administrator.', + withCredentials: true }); if (!resp) { return null; diff --git a/src/commons/utils/RequestHelper.tsx b/src/commons/utils/RequestHelper.tsx index f40008a762..c90b30187a 100644 --- a/src/commons/utils/RequestHelper.tsx +++ b/src/commons/utils/RequestHelper.tsx @@ -25,6 +25,7 @@ type RequestOptions = { noContentType?: boolean; noHeaderAccept?: boolean; refreshToken?: string; + withCredentials?: boolean; }; export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; @@ -140,16 +141,24 @@ export const generateApiCallHeadersAndFetchOptions = ( if (opts.accessToken) { headers.append('Authorization', `Bearer ${opts.accessToken}`); } - const fetchOpts: { method: RequestMethod; headers: Headers; body?: any } = { method, headers }; + const fetchOpts: { + method: RequestMethod; + headers: Headers; + body?: any; + credentials?: RequestCredentials; + } = { method, headers }; if (opts.body) { if (opts.noContentType) { // Content Type is not needed for sending multipart data - fetchOpts.body = opts.body; + fetchOpts.body = opts.body as any; } else { headers.append('Content-Type', 'application/json'); fetchOpts.body = JSON.stringify(opts.body); } } + if (opts.withCredentials) { + fetchOpts.credentials = 'include'; + } return fetchOpts; }; From 9b5c53f27f5516fee699e5ba2867e0e3922cefdd Mon Sep 17 00:00:00 2001 From: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> Date: Fri, 8 Dec 2023 20:26:27 +0700 Subject: [PATCH 3/3] Handle SAML login flow in frontend --- src/pages/login/Login.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx index e12201d87e..bd3921d308 100644 --- a/src/pages/login/Login.tsx +++ b/src/pages/login/Login.tsx @@ -14,6 +14,7 @@ import classNames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import { useLocation, useNavigate } from 'react-router'; +import { AuthProviderType } from 'src/commons/utils/AuthHelper'; import { useSession } from 'src/commons/utils/Hooks'; import { fetchAuth, login } from '../../commons/application/actions/SessionActions'; @@ -40,6 +41,8 @@ const Login: React.FunctionComponent<{}> = () => { [dispatch] ); + const isSaml = Constants.authProviders.get(providerId)?.type === AuthProviderType.SAML_SSO; + React.useEffect(() => { // If already logged in, navigate to relevant course page if (isLoggedIn) { @@ -52,12 +55,13 @@ const Login: React.FunctionComponent<{}> = () => { } // Else fetch JWT tokens and user info from backend when auth provider code is present - if (authCode && !isLoggedIn) { + // SAML does not require code, as relay is handled in backend + if ((authCode || isSaml) && !isLoggedIn) { dispatch(fetchAuth(authCode, providerId)); } - }, [authCode, providerId, dispatch, courseId, navigate, isLoggedIn]); + }, [authCode, isSaml, providerId, dispatch, courseId, navigate, isLoggedIn]); - if (authCode) { + if (authCode || isSaml) { return (