Skip to content

Commit

Permalink
DRYD-1244: Add SSO support. (#190)
Browse files Browse the repository at this point in the history
  • Loading branch information
ray-lee authored Sep 21, 2023
1 parent 5ee0d95 commit 3eec4c0
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 17 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cspace-ui",
"version": "9.0.0-dev.1",
"version": "9.0.0-dev.2",
"description": "CollectionSpace user interface for browsers",
"author": "Ray Lee <[email protected]>",
"license": "ECL-2.0",
Expand Down
25 changes: 16 additions & 9 deletions src/components/pages/service/PasswordResetRequestPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,50 +15,55 @@ import { isValidEmail } from '../../../helpers/validationHelpers';

const messages = defineMessages({
title: {
id: 'PasswordResetRequestPage.title',
id: 'passwordResetRequestPage.title',
description: 'Title of the password reset request page.',
defaultMessage: 'Reset Password',
},
prompt: {
id: 'PasswordResetRequestPage.prompt',
id: 'passwordResetRequestPage.prompt',
description: 'The prompt displayed on the password reset request page.',
defaultMessage: 'Please enter your email address to request a password reset.',
},
email: {
id: 'PasswordResetRequestPage.email',
id: 'passwordResetRequestPage.email',
description: 'Label for the email field on the password reset request page.',
defaultMessage: 'Email',
},
submit: {
id: 'PasswordResetRequestPage.submit',
id: 'passwordResetRequestPage.submit',
description: 'Label for the submit button on the password reset request page.',
defaultMessage: 'Submit',
},
success: {
id: 'PasswordResetRequestPage.success',
id: 'passwordResetRequestPage.success',
description: 'Message displayed when a password reset has been successfully requested.',
defaultMessage: 'An email has been sent to {email}. Follow the instructions in the email to finish resetting your password.',
},
error: {
id: 'PasswordResetRequestPage.error',
id: 'passwordResetRequestPage.error',
description: 'Generic message to display when a password reset request fails, and no more specific message is available.',
defaultMessage: 'An error occurred while attempting to request the password reset: {detail}',
},
errorNotFound: {
id: 'PasswordResetRequestPage.errorNotFound',
id: 'passwordResetRequestPage.errorNotFound',
description: 'Message to display when the email is not found for a password reset request.',
defaultMessage: 'Could not find an account with the email {email}.',
},
errorMissingEmail: {
id: 'PasswordResetRequestPage.errorMissingEmail',
id: 'passwordResetRequestPage.errorMissingEmail',
description: 'Message to display when no email is entered on the password reset request page.',
defaultMessage: 'Please enter an email address.',
},
errorInvalidEmail: {
id: 'PasswordResetRequestPage.errorInvalidEmail',
id: 'passwordResetRequestPage.errorInvalidEmail',
description: 'Message to display when the email entered on the password reset request page is not a valid email address.',
defaultMessage: '{email} is not a valid email address.',
},
errorSSORequired: {
id: 'passwordResetRequestPage.errorSSORequired',
description: 'Message to display on the password reset page when the account requires single sign-on.',
defaultMessage: '{email} is required to sign in using a single sign-on provider. The CollectionSpace account password cannot be reset.',
},
});

const propTypes = {
Expand Down Expand Up @@ -137,6 +142,8 @@ function PasswordResetRequestPage(props) {

if (response.status === 404) {
setError(<FormattedMessage {...messages.errorNotFound} values={{ email }} />);
} else if (/requires single sign-on/.test(text)) {
setError(<FormattedMessage {...messages.errorSSORequired} values={{ email }} />);
} else {
setError(<FormattedMessage {...messages.error} values={{ detail: text }} />);
}
Expand Down
32 changes: 32 additions & 0 deletions src/components/pages/service/ServiceLoginPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ const propTypes = {
error: PropTypes.string,
isLogoutSuccess: PropTypes.bool,
intl: intlShape.isRequired,
locale: PropTypes.string,
sso: PropTypes.object,
tenantId: PropTypes.string,
};

const defaultProps = {
csrf: null,
error: null,
isLogoutSuccess: false,
locale: 'en-US',
sso: {},
tenantId: null,
};

Expand Down Expand Up @@ -50,6 +54,10 @@ const messages = defineMessages({
description: 'Text of the forgot password link.',
defaultMessage: 'Forgot password',
},
ssoLink: {
id: 'serviceLoginPage.ssoLink',
defaultMessage: 'Continue with {name}',
},
localLogin: {
id: 'serviceLoginPage.localLogin',
defaultMessage: 'Continue with email and password',
Expand All @@ -70,9 +78,31 @@ function ServiceLoginPage(props) {
error,
isLogoutSuccess,
intl,
locale,
sso,
tenantId,
} = props;

const ssoLinks = Object.entries(sso)
.sort((a, b) => a[1].name.localeCompare(b[1].name, locale, { sensitivity: 'base' }))
.map(([url, config]) => {
const { icon } = config;

const style = icon
? { backgroundImage: `url(${icon})` }
: undefined;

return (
<a className="login" href={url} style={style}>
<FormattedMessage {...messages.ssoLink} values={config} />
</a>
);
});

const ssoPanel = (ssoLinks.length > 0)
? <div className="sso">{ssoLinks}</div>
: undefined;

const csrfInput = csrf
? <input type="hidden" name={csrf.parameterName} value={csrf.token} />
: undefined;
Expand Down Expand Up @@ -107,6 +137,8 @@ function ServiceLoginPage(props) {
<main>
<p><FormattedMessage {...messages.prompt} /></p>

{ssoPanel}

<form method="POST">
<div>
{/* Ignore an eslint misfire. */}
Expand Down
34 changes: 32 additions & 2 deletions src/plugins/recordTypes/account/fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const areRolesImmutable = ({ recordData }) => (

export default (configContext) => {
const {
CheckboxInput,
CompoundInput,
OptionPickerInput,
RolesInput,
Expand All @@ -31,6 +32,10 @@ export default (configContext) => {
configKey: config,
} = configContext.configHelpers;

const {
DATA_TYPE_BOOL,
} = configContext.dataTypes;

return {
'ns2:accounts_common': {
[config]: {
Expand Down Expand Up @@ -142,6 +147,25 @@ export default (configContext) => {
},
},
},
requireSSO: {
[config]: {
cloneable: false,
dataType: DATA_TYPE_BOOL,
defaultValue: false,
messages: defineMessages({
name: {
id: 'field.accounts_common.requireSSO.name',
defaultMessage: 'Require single sign-on (if available)',
},
}),
view: {
type: CheckboxInput,
props: {
readOnly: isMetadataImmutable,
},
},
},
},
password: {
[config]: {
cloneable: false,
Expand All @@ -156,7 +180,10 @@ export default (configContext) => {
defaultMessage: 'Password',
},
}),
required: ({ recordData }) => isNewRecord(recordData),
required: ({ recordData }) => (
isNewRecord(recordData)
&& recordData.getIn(['ns2:accounts_common', 'requireSSO']) === false
),
validate: ({ data, fieldDescriptor }) => {
if (data && !isValidPassword(data)) {
return {
Expand Down Expand Up @@ -185,7 +212,10 @@ export default (configContext) => {
defaultMessage: 'Confirm password',
},
}),
required: ({ recordData }) => isNewRecord(recordData),
required: ({ recordData }) => (
isNewRecord(recordData)
&& recordData.getIn(['ns2:accounts_common', 'requireSSO']) === false
),
view: {
type: PasswordInput,
props: {
Expand Down
1 change: 1 addition & 0 deletions src/plugins/recordTypes/account/forms/default.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const template = (configContext) => {
<Col>
<Field name="email" />
<Field name="screenName" />
<Field name="requireSSO" />
<Field name="password" />
<Field name="confirmPassword" />
<Field name="status" />
Expand Down
2 changes: 2 additions & 0 deletions src/service.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default (uiConfig) => {
locale,
logo,
messages,
sso,
tenantId,
token,
} = config;
Expand Down Expand Up @@ -84,6 +85,7 @@ export default (uiConfig) => {
error={error}
isLogoutSuccess={isLogoutSuccess}
locale={locale}
sso={sso}
tenantId={tenantId}
/>
)}
Expand Down
10 changes: 7 additions & 3 deletions styles/cspace-ui/ServicePage.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ body {
margin-top: 8px;
}

.common button {
.common :global(.sso) a, .common button {
display: block;
box-sizing: border-box;
outline-offset: -1px;
Expand All @@ -47,11 +47,11 @@ body {
font: inherit;
}

.common button:focus {
.common :global(.sso) a:focus, .common button:focus {
outline: 2px solid textDark;
}

.common button:enabled:hover {
.common :global(.sso) a:hover, .common button:enabled:hover {
background-color: #fff;
}

Expand All @@ -71,6 +71,10 @@ body {
background-image: url(../../images/lockReset.svg);
}

.common :global(.sso) + form {
margin-top: 36px;
}

.common form {
box-sizing: border-box;
margin-top: 16px;
Expand Down

0 comments on commit 3eec4c0

Please sign in to comment.