Skip to content

Add support for CAS auth strategy #2246

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Nov 30, 2023
Merged
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down
5 changes: 3 additions & 2 deletions src/commons/sagas/__tests__/BackendSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);

Expand Down
14 changes: 13 additions & 1 deletion src/commons/utils/AuthHelper.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
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) {
return 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
Expand Down
22 changes: 19 additions & 3 deletions src/commons/utils/Constants.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -46,8 +48,10 @@ const caFulfillmentLevel = isTest
? 24
: parseInt(process.env.REACT_APP_CA_FULFILLMENT_LEVEL || '0');

const authProviders: Map<string, { name: string; endpoint: string; isDefault: boolean }> =
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}`];
Expand All @@ -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 {
Expand Down
13 changes: 8 additions & 5 deletions src/pages/login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand All @@ -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 (
<div className={classNames('Login', Classes.DARK)}>
<Card className={classNames('login-card', Classes.ELEVATION_4)}>
Expand Down