Skip to content
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

Support SAML authentication provider #2719

Merged
merged 3 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
Expand Down
3 changes: 2 additions & 1 deletion src/commons/sagas/RequestsSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion src/commons/utils/AuthHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions src/commons/utils/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}
RichDom2185 marked this conversation as resolved.
Show resolved Hide resolved

export enum Links {
githubIssues = 'https://github.com/source-academy/frontend/issues',
githubOrg = 'https://github.com/source-academy',
Expand Down
13 changes: 11 additions & 2 deletions src/commons/utils/RequestHelper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type RequestOptions = {
noContentType?: boolean;
noHeaderAccept?: boolean;
refreshToken?: string;
withCredentials?: boolean;
};

export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
Expand Down Expand Up @@ -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;
};
Expand Down
10 changes: 7 additions & 3 deletions src/pages/login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand All @@ -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 (
<div className={classNames('Login', Classes.DARK)}>
<Card className={classNames('login-card', Classes.ELEVATION_4)}>
Expand Down