From 6ebe68efb2045bf4694569f7f375eaf753c19dc0 Mon Sep 17 00:00:00 2001 From: En Rong <53928333+chownces@users.noreply.github.com> Date: Thu, 30 Nov 2023 08:02:34 +0800 Subject: [PATCH] Add support for CAS auth strategy (#2246) * add cas auth strategy * fix test case --------- Co-authored-by: Richard Dominick <34370238+RichDom2185@users.noreply.github.com> --- .env.example | 4 ++++ src/commons/sagas/__tests__/BackendSaga.ts | 5 +++-- src/commons/utils/AuthHelper.ts | 14 +++++++++++++- src/commons/utils/Constants.ts | 22 +++++++++++++++++++--- src/pages/login/Login.tsx | 13 ++++++++----- 5 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 6742e52877..13c8fa2556 100644 --- a/.env.example +++ b/.env.example @@ -37,6 +37,10 @@ REACT_APP_OAUTH2_PROVIDER3_ENDPOINT=http://localhost:8000/login?provider=test&co # REACT_APP_OAUTH2_PROVIDER2_NAME=Cognito # REACT_APP_OAUTH2_PROVIDER2_ENDPOINT= +REACT_APP_CAS_PROVIDER1= +REACT_APP_CAS_PROVIDER1_NAME= +REACT_APP_CAS_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/sagas/__tests__/BackendSaga.ts b/src/commons/sagas/__tests__/BackendSaga.ts index b0ee85603d..164d0d57ff 100644 --- a/src/commons/sagas/__tests__/BackendSaga.ts +++ b/src/commons/sagas/__tests__/BackendSaga.ts @@ -78,7 +78,7 @@ import { import { mockGradingSummary } from '../../mocks/GradingMocks'; import { mockNotifications } from '../../mocks/UserMocks'; import { Notification } from '../../notificationBadge/NotificationBadgeTypes'; -import { computeRedirectUri } from '../../utils/AuthHelper'; +import { AuthProviderType, computeRedirectUri } from '../../utils/AuthHelper'; import Constants from '../../utils/Constants'; import { showSuccessMessage, @@ -286,7 +286,8 @@ describe('Test FETCH_AUTH action', () => { Constants.authProviders.set(providerId, { name: providerId, endpoint: `https://test/?client_id=${clientId}`, - isDefault: true + isDefault: true, + type: AuthProviderType.OAUTH2 }); const redirectUrl = computeRedirectUri(providerId); diff --git a/src/commons/utils/AuthHelper.ts b/src/commons/utils/AuthHelper.ts index 0fef8cddb3..c8ec30669d 100644 --- a/src/commons/utils/AuthHelper.ts +++ b/src/commons/utils/AuthHelper.ts @@ -1,5 +1,10 @@ import Constants from './Constants'; +export enum AuthProviderType { + OAUTH2 = 'OAUTH2', + CAS = 'CAS' +} + export function computeEndpointUrl(providerId: string): string | undefined { const ep = Constants.authProviders.get(providerId); if (!ep) { @@ -7,7 +12,14 @@ export function computeEndpointUrl(providerId: string): string | undefined { } try { const epUrl = new URL(ep.endpoint); - epUrl.searchParams.set('redirect_uri', computeRedirectUri(providerId)!); + switch (ep.type) { + case AuthProviderType.OAUTH2: + epUrl.searchParams.set('redirect_uri', computeRedirectUri(providerId)!); + break; + case AuthProviderType.CAS: + epUrl.searchParams.set('service', computeRedirectUri(providerId)!); + break; + } return epUrl.toString(); } catch (e) { // in dev, sometimes the endpoint is a dummy; allow that diff --git a/src/commons/utils/Constants.ts b/src/commons/utils/Constants.ts index 10b4c0a0fa..aff8fbcd26 100644 --- a/src/commons/utils/Constants.ts +++ b/src/commons/utils/Constants.ts @@ -1,5 +1,7 @@ import { Chapter, Variant } from 'js-slang/dist/types'; +import { AuthProviderType } from './AuthHelper'; + function isTrue(value?: string, defaultTo?: boolean): boolean { return typeof value === 'undefined' && typeof defaultTo !== 'undefined' ? defaultTo @@ -46,8 +48,10 @@ const caFulfillmentLevel = isTest ? 24 : parseInt(process.env.REACT_APP_CA_FULFILLMENT_LEVEL || '0'); -const authProviders: Map = - new Map(); +const authProviders: Map< + string, + { name: string; endpoint: string; isDefault: boolean; type: AuthProviderType } +> = new Map(); for (let i = 1; ; ++i) { const id = process.env[`REACT_APP_OAUTH2_PROVIDER${i}`]; @@ -58,7 +62,19 @@ for (let i = 1; ; ++i) { const name = process.env[`REACT_APP_OAUTH2_PROVIDER${i}_NAME`] || 'Unnamed provider'; const endpoint = process.env[`REACT_APP_OAUTH2_PROVIDER${i}_ENDPOINT`] || ''; - authProviders.set(id, { name, endpoint, isDefault: i === 1 }); + authProviders.set(id, { name, endpoint, isDefault: i === 1, type: AuthProviderType.OAUTH2 }); +} + +for (let i = 1; ; ++i) { + const id = process.env[`REACT_APP_CAS_PROVIDER${i}`]; + if (!id) { + break; + } + + const name = process.env[`REACT_APP_CAS_PROVIDER${i}_NAME`] || 'Unnamed provider'; + const endpoint = process.env[`REACT_APP_CAS_PROVIDER${i}_ENDPOINT`] || ''; + + authProviders.set(id, { name, endpoint, isDefault: false, type: AuthProviderType.CAS }); } export enum Links { diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx index 67501081a9..e12201d87e 100644 --- a/src/pages/login/Login.tsx +++ b/src/pages/login/Login.tsx @@ -30,7 +30,10 @@ const Login: React.FunctionComponent<{}> = () => { const location = useLocation(); const { isLoggedIn, courseId } = useSession(); const navigate = useNavigate(); - const { code, provider: providerId } = parseQuery(location.search); + const { code, ticket, provider: providerId } = parseQuery(location.search); + + // `code` parameter from OAuth2 redirect, `ticket` from CAS redirect + const authCode = code || ticket; const handleLogin = React.useCallback( (providerId: string) => dispatch(login(providerId)), @@ -49,12 +52,12 @@ const Login: React.FunctionComponent<{}> = () => { } // Else fetch JWT tokens and user info from backend when auth provider code is present - if (code && !isLoggedIn) { - dispatch(fetchAuth(code, providerId)); + if (authCode && !isLoggedIn) { + dispatch(fetchAuth(authCode, providerId)); } - }, [code, providerId, dispatch, courseId, navigate, isLoggedIn]); + }, [authCode, providerId, dispatch, courseId, navigate, isLoggedIn]); - if (code) { + if (authCode) { return (