Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@
*
*/
public enum EmailVerificationStatus {
VERIFIED, NOT_VERIFIED, DELIVERY_FAILED;
VERIFIED, NOT_VERIFIED, DELIVERY_FAILED, AGE_RESTRICTED;
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,7 @@
import uk.ac.cam.cl.dtg.segue.auth.IAuthenticator;
import uk.ac.cam.cl.dtg.segue.auth.IPasswordAuthenticator;
import uk.ac.cam.cl.dtg.segue.auth.ISecondFactorAuthenticator;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.AdditionalAuthenticationRequiredException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.AuthenticationCodeException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.AuthenticationProviderMappingException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.AuthenticatorSecurityException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.CodeExchangeException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.CrossSiteRequestForgeryException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.DuplicateAccountException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.FailedToHashPasswordException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.IncorrectCredentialsProvidedException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.InvalidNameException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.InvalidPasswordException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.InvalidSessionException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.InvalidTokenException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.MFARequiredButNotConfiguredException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.MissingRequiredFieldException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoCredentialsAvailableException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.NoUserLoggedInException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.UnknownCountryCodeException;
import uk.ac.cam.cl.dtg.segue.auth.exceptions.*;
import uk.ac.cam.cl.dtg.segue.comm.CommunicationException;
import uk.ac.cam.cl.dtg.segue.comm.EmailManager;
import uk.ac.cam.cl.dtg.segue.comm.EmailMustBeVerifiedException;
Expand Down Expand Up @@ -641,6 +623,9 @@ public Response updateUserObject(final HttpServletRequest request, final HttpSer
} catch (UnknownCountryCodeException e) {
log.warn("Unknown country code provided during user update.");
return new SegueErrorResponse(Response.Status.BAD_REQUEST, e.getMessage()).toResponse();
} catch (AgeRestrictedException e) {
log.warn("User update failed due to age restriction.");
return new SegueErrorResponse(Response.Status.BAD_REQUEST, e.getMessage()).toResponse();
}
}

Expand Down Expand Up @@ -1085,7 +1070,7 @@ public RegisteredUserDTO createUserObjectAndSession(final HttpServletRequest req
*/
public RegisteredUserDTO updateUserObject(final RegisteredUser updatedUser, final String newPassword)
throws InvalidPasswordException, MissingRequiredFieldException, SegueDatabaseException,
InvalidKeySpecException, NoSuchAlgorithmException, InvalidNameException, UnknownCountryCodeException {
InvalidKeySpecException, NoSuchAlgorithmException, InvalidNameException, UnknownCountryCodeException, AgeRestrictedException {
Objects.requireNonNull(updatedUser.getId());

// We want to map to DTO first to make sure that the user cannot
Expand Down Expand Up @@ -1151,13 +1136,24 @@ public RegisteredUserDTO updateUserObject(final RegisteredUser updatedUser, fina
userToSave.setDateOfBirth(null);
}

// Before save we should validate the user for mandatory fields.
// Before save, we should validate the user for mandatory fields.

// First, validate the user's email if their account is not age restricted.
// Doing this before the email change code is necessary to ensure that (a) users cannot try and change to an
// invalid email, and (b) that users with an invalid email can change their email to a valid one!
if (!isUserEmailValid(userToSave.getEmail())) {
throw new MissingRequiredFieldException("The email address provided is invalid.");

if (existingUser.getEmailVerificationStatus() != EmailVerificationStatus.AGE_RESTRICTED) {
if (!isUserEmailValid(userToSave.getEmail())) {
throw new MissingRequiredFieldException("The email address provided is invalid.");
}
} else {
// If the user is age restricted, do not allow changing the email.
if (!existingUser.getEmail().equals(userToSave.getEmail())) {
throw new AgeRestrictedException("Your account email cannot be set while your account is age restricted.");
}
}


// Make sure the email address is preserved (can't be changed until new email is verified)
// Send a new verification email if the user has changed their email
if (!existingUser.getEmail().equals(updatedUser.getEmail())) {
Expand Down Expand Up @@ -1829,7 +1825,8 @@ private RegisteredUser registerUserWithFederatedProvider(final AuthenticationPro

// since the federated providers didn't always provide email addresses - we have to check and update accordingly.
if (!localUserInformation.getEmail().contains("@")
&& !EmailVerificationStatus.DELIVERY_FAILED.equals(localUserInformation.getEmailVerificationStatus())) {
&& !EmailVerificationStatus.DELIVERY_FAILED.equals(localUserInformation.getEmailVerificationStatus())
&& !EmailVerificationStatus.AGE_RESTRICTED.equals(localUserInformation.getEmailVerificationStatus())) {
this.updateUserEmailVerificationStatus(localUserInformation.getEmail(),
EmailVerificationStatus.DELIVERY_FAILED);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,41 +160,68 @@ public synchronized UserFromAuthProvider getUserInfo(String internalProviderRefe
throw new AuthenticatorSecurityException("ID token is invalid - possible indication of tampering.");
}

boolean isUnder13 = idToken.getPayload().get("email").equals("dummy@example.com");

// Read claims from the ID token - no need to go to the UserInfo endpoint, as everything is here
// Names are obtained differently across u13 and o13 users, so handled separately below
String sub = (String) idToken.getPayload().get("sub");
String fullName = (String) idToken.getPayload().get("name");
String nickname = (String) idToken.getPayload().get("nickname");
String email = (String) idToken.getPayload().get("email");
String country = (String) idToken.getPayload().get("country_code");

boolean emailVerified = (Boolean) idToken.getPayload().getOrDefault("email_verified", false);

if (null != country && !CountryLookupManager.isKnownCountryCode(country)) {
log.debug("Country code '{}' from identity provider is not known, discarding.", country);
country = null;
}

if (null == nickname || null == fullName || null == email || null == sub) {
throw new NoUserException("Required field missing from identity provider's response.");
}
else {
// Build a given name/family name based on the nickname and full name fields available. This makes
// unreasonable assumptions about the structure of names, but it's the best we can do.
List<String> givenNameFamilyName = getGivenNameFamilyName(nickname, fullName);

EmailVerificationStatus emailStatus = emailVerified ? EmailVerificationStatus.VERIFIED : EmailVerificationStatus.NOT_VERIFIED;

// Use the IdP's unique ID for the user ('sub') as the unique (per identity provider) user ID.
return new UserFromAuthProvider(
sub,
givenNameFamilyName.get(0),
givenNameFamilyName.get(1),
email,
emailStatus,
null,
null,
null,
country,
false);
if (isUnder13) {
String username = (String) idToken.getPayload().get("username");

if (null == username || null == email || null == sub) {
throw new NoUserException("Required field missing from identity provider's response for under-13 user.");
} else {
// We cannot reasonably infer any part of a name from their username. Instead, use a blank given
// name and the username as the family name. The user is expected to change this in the signup flow.
return new UserFromAuthProvider(
sub,
null,
username,
username + "-rpf",
EmailVerificationStatus.AGE_RESTRICTED,
null,
null,
null,
country,
false
);
}
} else {
String fullName = (String) idToken.getPayload().get("name");
String nickname = (String) idToken.getPayload().get("nickname");

if (null == nickname || null == fullName || null == email || null == sub) {
throw new NoUserException("Required field missing from identity provider's response.");
} else {
// Build a given name/family name based on the nickname and full name fields available. This makes
// unreasonable assumptions about the structure of names, but it's the best we can do.
List<String> givenNameFamilyName = getGivenNameFamilyName(nickname, fullName);

EmailVerificationStatus emailStatus = emailVerified ? EmailVerificationStatus.VERIFIED : EmailVerificationStatus.NOT_VERIFIED;

// Use the IdP's unique ID for the user ('sub') as the unique (per identity provider) user ID.
return new UserFromAuthProvider(
sub,
givenNameFamilyName.get(0),
givenNameFamilyName.get(1),
email,
emailStatus,
null,
null,
null,
country,
false);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package uk.ac.cam.cl.dtg.segue.auth.exceptions;

/**
* An exception to indicate that the user is age restricted from accessing the service.
*
* @author Jaycie Brown
*
*/
public class AgeRestrictedException extends Exception {
public AgeRestrictedException(final String message) {
super(message);
}
}
5 changes: 5 additions & 0 deletions src/main/java/uk/ac/cam/cl/dtg/segue/comm/EmailManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,11 @@ private boolean filterByPreferencesAndAddToQueue(final RegisteredUserDTO userDTO
return false;
}

if (userDTO.getEmailVerificationStatus() == EmailVerificationStatus.AGE_RESTRICTED) {
log.info("Email sending abandoned - verification status is AGE_RESTRICTED");
return false;
}

// if this is an email type that cannot have a preference, send it and log as appropriate
if (!email.getEmailType().isValidEmailPreference()) {
log.info("Added {} email to the queue with subject: '{}'.", email.getEmailType().toString().toLowerCase(), email.getSubject());
Expand Down