Skip to content

Commit eeef575

Browse files
committed
Merge branch 'feature/user-consent' into feature/newCAS-v21.0.0-alpha
2 parents 1a32137 + 9c4166f commit eeef575

17 files changed

Lines changed: 315 additions & 56 deletions

src/main/java/io/cos/cas/osf/authentication/credential/OsfPostgresCredential.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public class OsfPostgresCredential extends RememberMeUsernamePasswordCredential
3838

3939
public static String AUTHENTICATION_ATTRIBUTE_REMEMBER_ME = "rememberMe";
4040

41+
public static String AUTHENTICATION_ATTRIBUTE_TOS_CONSENT = "termsOfServiceChecked";
42+
4143
private static String DEFAULT_INSTITUTION_ID = "none";
4244

4345
private static DelegationProtocol DEFAULT_DELEGATION_PROTOCOL = DelegationProtocol.NONE;
@@ -52,6 +54,11 @@ public class OsfPostgresCredential extends RememberMeUsernamePasswordCredential
5254
*/
5355
private String oneTimePassword;
5456

57+
/**
58+
* The boolean flag that indicates whether the user has checked the terms of service consent agreement
59+
*/
60+
private boolean termsOfServiceChecked;
61+
5562
/**
5663
* The boolean flag that indicates successful delegated authentication if true.
5764
*/
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.cos.cas.osf.authentication.exception;
2+
3+
import lombok.NoArgsConstructor;
4+
5+
import javax.security.auth.login.AccountException;
6+
7+
/**
8+
* Describes an authentication error condition where a user account needs to agree to OSF's terms of service.
9+
*
10+
* @author Longze Chen
11+
* @since 21.1.0
12+
*/
13+
@NoArgsConstructor
14+
public class TermsOfServiceConsentRequiredException extends AccountException {
15+
16+
/**
17+
* Serialization metadata.
18+
*/
19+
private static final long serialVersionUID = -7702088330316457626L;
20+
21+
/**
22+
* Instantiates a new {@link TermsOfServiceConsentRequiredException}.
23+
*
24+
* @param msg the msg
25+
*/
26+
public TermsOfServiceConsentRequiredException(final String msg) {
27+
super(msg);
28+
}
29+
}

src/main/java/io/cos/cas/osf/authentication/handler/support/OsfPostgresAuthenticationHandler.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import io.cos.cas.osf.authentication.credential.OsfPostgresCredential;
44
import io.cos.cas.osf.authentication.exception.AccountNotConfirmedIdpException;
55
import io.cos.cas.osf.authentication.exception.AccountNotConfirmedOsfException;
6-
import io.cos.cas.osf.authentication.exception.InstitutionSsoFailedException;
76
import io.cos.cas.osf.authentication.exception.InvalidOneTimePasswordException;
87
import io.cos.cas.osf.authentication.exception.InvalidPasswordException;
98
import io.cos.cas.osf.authentication.exception.InvalidUserStatusException;
109
import io.cos.cas.osf.authentication.exception.OneTimePasswordRequiredException;
1110
import io.cos.cas.osf.authentication.exception.InvalidVerificationKeyException;
11+
import io.cos.cas.osf.authentication.exception.TermsOfServiceConsentRequiredException;
1212
import io.cos.cas.osf.authentication.support.DelegationProtocol;
1313
import io.cos.cas.osf.authentication.support.OsfUserStatus;
1414
import io.cos.cas.osf.authentication.support.OsfUserUtils;
@@ -111,6 +111,7 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfPostgresInte
111111
final String oneTimePassword = credential.getOneTimePassword();
112112
final String institutionId = credential.getInstitutionId();
113113
final boolean isRememberMe = credential.isRememberMe();
114+
final boolean isTermsOfServiceChecked = credential.isTermsOfServiceChecked();
114115
final boolean isRemotePrincipal = credential.isRemotePrincipal();
115116
final DelegationProtocol delegationProtocol = credential.getDelegationProtocol();
116117

@@ -169,6 +170,11 @@ protected final AuthenticationHandlerExecutionResult authenticateOsfPostgresInte
169170
}
170171
}
171172

173+
if (!osfUser.isTermsOfServiceAccepted() && !isTermsOfServiceChecked) {
174+
LOGGER.info("Terms of service consent is required for [" + username + "]");
175+
throw new TermsOfServiceConsentRequiredException("Terms of service consent is required for [" + username + "]");
176+
}
177+
172178
if (OsfUserStatus.USER_NOT_CONFIRMED_OSF.equals(userStatus)) {
173179
throw new AccountNotConfirmedOsfException(
174180
"User [" + username + "] is registered via OSF but not confirmed"

src/main/java/io/cos/cas/osf/authentication/metadata/OsfPostgresAuthenticationMetaDataPopulator.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,22 @@ public void populateAttributes(final AuthenticationBuilder builder, final Authen
2727
transaction.getPrimaryCredential().ifPresent(r -> {
2828
final OsfPostgresCredential credential = (OsfPostgresCredential) r;
2929
LOGGER.debug(
30-
"Credential is of type [{}], thus adding attributes [{}, {}, {}, {}]",
30+
"Credential is of type [{}], thus adding attributes [{}, {}, {}, {}, {}]",
3131
OsfPostgresCredential.class.getSimpleName(),
3232
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMEMBER_ME,
3333
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMOTE_PRINCIPAL,
3434
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_DELEGATION_PROTOCOL,
35-
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_INSTITUTION_ID
35+
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_INSTITUTION_ID,
36+
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_TOS_CONSENT
3637
);
3738
builder.addAttribute(
3839
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMEMBER_ME,
3940
credential.isRememberMe()
4041
);
42+
builder.addAttribute(
43+
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_TOS_CONSENT,
44+
credential.isTermsOfServiceChecked()
45+
);
4146
builder.addAttribute(
4247
OsfPostgresCredential.AUTHENTICATION_ATTRIBUTE_REMOTE_PRINCIPAL,
4348
credential.isRemotePrincipal()

src/main/java/io/cos/cas/osf/model/OsfUser.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public final class OsfUser extends AbstractOsfModel {
6262
@Column(name = "date_confirmed")
6363
private Date dateConfirmed;
6464

65+
@Temporal(TemporalType.TIMESTAMP)
66+
@Column(name = "accepted_terms_of_service")
67+
private Date dateTermsOfServiceAccepted;
68+
6569
@Temporal(TemporalType.TIMESTAMP)
6670
@Column(name = "date_disabled")
6771
private Date dateDisabled;
@@ -86,6 +90,10 @@ public boolean isConfirmed() {
8690
return dateConfirmed != null;
8791
}
8892

93+
public boolean isTermsOfServiceAccepted() {
94+
return dateTermsOfServiceAccepted != null;
95+
}
96+
8997
public boolean isDisabled() {
9098
return dateDisabled != null;
9199
}

src/main/java/io/cos/cas/osf/web/flow/config/OsfCasCoreWebflowConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.cos.cas.osf.authentication.exception.InvalidUserStatusException;
99
import io.cos.cas.osf.authentication.exception.InvalidVerificationKeyException;
1010
import io.cos.cas.osf.authentication.exception.OneTimePasswordRequiredException;
11+
import io.cos.cas.osf.authentication.exception.TermsOfServiceConsentRequiredException;
1112

1213
import org.apereo.cas.configuration.CasConfigurationProperties;
1314
import org.apereo.cas.web.flow.config.CasCoreWebflowConfiguration;
@@ -47,6 +48,7 @@ public Set<Class<? extends Throwable>> handledAuthenticationExceptions() {
4748
errors.add(InvalidUserStatusException.class);
4849
errors.add(InvalidVerificationKeyException.class);
4950
errors.add(OneTimePasswordRequiredException.class);
51+
errors.add(TermsOfServiceConsentRequiredException.class);
5052

5153
// Add built-in exceptions after OSF-specific exceptions since order matters
5254
errors.addAll(super.handledAuthenticationExceptions());

src/main/java/io/cos/cas/osf/web/flow/configurer/OsfCasLoginWebflowConfigurer.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.cos.cas.osf.authentication.exception.InvalidUserStatusException;
99
import io.cos.cas.osf.authentication.exception.InvalidVerificationKeyException;
1010
import io.cos.cas.osf.authentication.exception.OneTimePasswordRequiredException;
11+
import io.cos.cas.osf.authentication.exception.TermsOfServiceConsentRequiredException;
1112
import io.cos.cas.osf.web.flow.support.OsfCasWebflowConstants;
1213

1314
import org.apereo.cas.authentication.PrincipalException;
@@ -79,6 +80,7 @@ protected void createDefaultViewStates(final Flow flow) {
7980
super.createDefaultViewStates(flow);
8081
// Create OSF customized view states
8182
createTwoFactorLoginFormView(flow);
83+
createTermsOfServiceConsentLoginFormView(flow);
8284
createInstitutionLoginView(flow);
8385
createOrcidLoginAutoRedirectView(flow);
8486
createDefaultServiceLoginAutoRedirectView(flow);
@@ -246,6 +248,11 @@ protected void createHandleAuthenticationFailureAction(final Flow flow) {
246248
InvalidOneTimePasswordException.class.getSimpleName(),
247249
OsfCasWebflowConstants.VIEW_ID_ONE_TIME_PASSWORD_REQUIRED
248250
);
251+
createTransitionForState(
252+
handler,
253+
TermsOfServiceConsentRequiredException.class.getSimpleName(),
254+
OsfCasWebflowConstants.VIEW_ID_TERMS_OF_SERVICE_CONSENT_REQUIRED
255+
);
249256
createTransitionForState(
250257
handler,
251258
InstitutionSsoFailedException.class.getSimpleName(),
@@ -427,6 +434,40 @@ private void createTwoFactorLoginFormView(final Flow flow) {
427434
attributes.put("history", History.INVALIDATE);
428435
}
429436

437+
/**
438+
* Create the customized terms of service consent form submission view state for OSF CAS.
439+
*
440+
* @param flow the flow
441+
*/
442+
private void createTermsOfServiceConsentLoginFormView(final Flow flow) {
443+
List<String> propertiesToBind = CollectionUtils.wrapList("termsOfServiceChecked", "source");
444+
BinderConfiguration binder = createStateBinderConfiguration(propertiesToBind);
445+
casProperties.getView().getCustomLoginFormFields()
446+
.forEach((field, props) -> {
447+
String fieldName = String.format("customFields[%s]", field);
448+
binder.addBinding(
449+
new BinderConfiguration.Binding(fieldName, props.getConverter(), props.isRequired())
450+
);
451+
});
452+
ViewState state = createViewState(
453+
flow,
454+
OsfCasWebflowConstants.VIEW_ID_TERMS_OF_SERVICE_CONSENT_REQUIRED,
455+
OsfCasWebflowConstants.VIEW_ID_TERMS_OF_SERVICE_CONSENT_REQUIRED,
456+
binder
457+
);
458+
state.getRenderActionList().add(createEvaluateAction(CasWebflowConstants.ACTION_ID_RENDER_LOGIN_FORM));
459+
createStateModelBinding(state, CasWebflowConstants.VAR_ID_CREDENTIAL, OsfPostgresCredential.class);
460+
Transition transition = createTransitionForState(
461+
state,
462+
CasWebflowConstants.TRANSITION_ID_SUBMIT,
463+
CasWebflowConstants.STATE_ID_REAL_SUBMIT
464+
);
465+
MutableAttributeMap<Object> attributes = transition.getAttributes();
466+
attributes.put("bind", Boolean.TRUE);
467+
attributes.put("validate", Boolean.TRUE);
468+
attributes.put("history", History.INVALIDATE);
469+
}
470+
430471
/**
431472
* Create the ORCiD login auto-redirect view to support the OSF feature "sign-up via ORCiD".
432473
*

src/main/java/io/cos/cas/osf/web/flow/support/OsfCasWebflowConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public interface OsfCasWebflowConstants {
4242

4343
String VIEW_ID_ONE_TIME_PASSWORD_REQUIRED = "casTwoFactorLoginView";
4444

45+
String VIEW_ID_TERMS_OF_SERVICE_CONSENT_REQUIRED = "casTermsOfServiceConsentView";
46+
4547
String VIEW_ID_ACCOUNT_NOT_CONFIRMED_OSF = "casAccountNotConfirmedOsfView";
4648

4749
String VIEW_ID_ACCOUNT_NOT_CONFIRMED_IDP = "casAccountNotConfirmedIdPView";

src/main/resources/messages.properties

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,10 @@ copyright.cos=<span style="white-space: nowrap">Copyright &copy; 2011 &ndash; 20
529529
#
530530
screen.welcome.label.loginwith=Sign in through external identity providers
531531
#
532+
# Texts and messages that are shared across all pages
533+
#
534+
screen.generic.button.wip = One moment please ...
535+
#
532536
# Login page and login form submission
533537
#
534538
cas.login.pagetitle=Sign in
@@ -547,17 +551,33 @@ screen.delegation.button.institution=Sign in through institution
547551
screen.delegation.heading.orcidredirect=Redirecting to ORCiD
548552
screen.flowless.heading.defaultserviceredirect=Redirecting to OSF login
549553
#
550-
# Two factor and login form submission
554+
# Two factor login form submission
551555
#
552-
cas.twofactor.pagetitle=Sign in
556+
screen.twofactor.pagetitle=Sign in
553557
screen.twofactor.instructions.top=Enter your one-time password to finish login
558+
screen.twofactor.label.email=OSF account email
554559
screen.twofactor.label.onetimepassword=<span class="accesskey">O</span>ne-time password (6-digit)
555560
screen.twofactor.label.onetimepassword.accesskey=o
556561
screen.twofactor.button.verify=Verify
557-
screen.twofactor.button.verifywip=One moment please...
558562
screen.twofactor.button.cancel=Cancel
559563
screen.twofactor.instructions.bottom=Open the two-factor authentication app on your device to view your authentication code and verify your identity.
560564
#
565+
# Terms of service consent check login form submission
566+
#
567+
screen.tosconsent.pagetitle=Terms of service
568+
screen.tosconsent.instructions.top=Terms of use and privacy policy
569+
screen.tosconsent.message.p1=You must read and agree to our \
570+
<a style="white-space: nowrap" href="https://github.com/CenterForOpenScience/centerforopenscience.org/blob/master/TERMS_OF_USE.md>Terms of Use"</a> and \
571+
<a style="white-space: nowrap" href="https://github.com/CenterForOpenScience/centerforopenscience.org/blob/master/PRIVACY_POLICY.md>Privacy Policy"</a> \
572+
to finish login.
573+
screen.tosconsent.message.p2=You are seeing this page, either because this is your first-time login to OSF via your \
574+
institution, or because we have recently updated the terms.
575+
screen.tosconsent.message.p3=Please read them carefully and contact \
576+
<a style="white-space: nowrap" href="mailto:support@osf.io">OSF Support</a> should you have any questions.
577+
screen.tosconsent.checkbox.title=I have read and agree to these terms.
578+
screen.tosconsent.button.agree=Continue
579+
screen.tosconsent.link.cancel=Cancel and go back to OSF
580+
#
561581
# Institution login page
562582
#
563583
screen.institutionlogin.pagetitle=Institution SSO
@@ -571,9 +591,6 @@ screen.institutionlogin.select.errormessage=You must select an institution.
571591
screen.institutionlogin.button.submit=Sign in
572592
screen.institutionlogin.osf=Sign in with OSF
573593
screen.institutionlogin.backtoosf=Exit and go back to OSF
574-
screen.institutionlogin.consent.checkbox=I have read and agree to the <a style="white-space: nowrap" href=https://github.com/CenterForOpenScience/centerforopenscience.org/blob/master/TERMS_OF_USE.md>Terms of Use</a> and <a style="white-space: nowrap" href=https://github.com/CenterForOpenScience/centerforopenscience.org/blob/master/PRIVACY_POLICY.md>Privacy Policy</a>.
575-
screen.institutionlogin.consent.errormessage=You must read and agree to the <span style="white-space: nowrap">Terms of Use</span> and <span style="white-space: nowrap">Privacy Policy</span>.
576-
577594
#
578595
# Generic login and logout success page
579596
#
@@ -594,6 +611,7 @@ screen.generic.button.hidedetails=Hide authentication details
594611
username.required=Email is required.
595612
password.required=Password is required.
596613
oneTimePassword.required=One-time password is required.
614+
termsOfServiceChecked.required=Terms of service consent is required.
597615
authenticationFailure.AccountDisabledException=This account has been disabled.
598616
authenticationFailure.AccountNotFoundException=The email or password you entered is incorrect.
599617
authenticationFailure.AccountNotConfirmedOsfException=The account you tried to log in to has not been confirmed.
@@ -605,6 +623,7 @@ authenticationFailure.InvalidPasswordException=The email or password you entered
605623
authenticationFailure.InvalidVerificationKeyException=The verification key you entered is incorrect.
606624
authenticationFailure.InvalidUserStatusException=The account you tried to log in to is not active.
607625
authenticationFailure.OneTimePasswordRequiredException=
626+
authenticationFailure.TermsOfServiceConsentRequiredException=
608627
#
609628
# Authentication exception messages in stand-alone exception views
610629
#

src/main/resources/static/css/cas.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ button.close {
593593
--cas-theme-osf-green: #357935;
594594
--cas-theme-osf-blue: #1b6d85;
595595
--cas-theme-osf-red: #b52b27;
596+
--cas-theme-osf-disabled: #EFEFEF;
596597
--cas-theme-primary: var(--cas-theme-osf-navbar, #263947);
597598
--cas-theme-danger: var(--cas-theme-osf-red, #b52b27);
598599
--mdc-theme-primary: var(--cas-theme-primary, #263947);
@@ -811,6 +812,11 @@ body {
811812
background-color: var(--cas-theme-osf-red, #b52b27);
812813
}
813814

815+
.form-button .button-osf-disabled,
816+
.form-button-inline .button-osf-disabled {
817+
background-color: var(--cas-theme-osf-disabled, #EFEFEF);
818+
}
819+
814820
.login-error-inline {
815821
margin: 0.25rem 0;
816822
}

0 commit comments

Comments
 (0)