Skip to content

Commit

Permalink
Merge branch 'dev' into epic/cognito/develop
Browse files Browse the repository at this point in the history
  • Loading branch information
sbearcsiro committed Dec 15, 2022
2 parents 542166e + 4d07ffe commit 1a62b43
Show file tree
Hide file tree
Showing 19 changed files with 101,333 additions and 107 deletions.
2 changes: 2 additions & 0 deletions userdetails-plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ dependencies {
exclude module: 'bootstrap' // dependency from skin
}

implementation 'org.passay:passay:1.6.0'

api('au.org.ala.plugins:openapi:1.1.0')

testImplementation('com.squareup.retrofit2:retrofit-mock:2.9.0')
Expand Down
23 changes: 23 additions & 0 deletions userdetails-plugin/grails-app/conf/plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,24 @@ spring:

password:
encoder: bcrypt # or legacy
generatedLength: 10
# Passwords must satisfy this policy.
# The minLength policy is always required, even when the policy is disabled. It has a default value of 8.
# To remove / disable an aspect of the policy, either remove the item or set to 0 / false, whichever is relevant.
policy:
minLength: 8
enabled: true
maxLength: 64
excludeUsername: true
excludeUsQwertyKeyboardSequence: true
excludeCommonPasswords: true
charGroupMinRequired: 3
charGroupMinUpperCase: 1
charGroupMinLowerCase: 1
charGroupMinUpperOrLowerCase: 0
charGroupMinDigit: 1
charGroupMinSpecial: 1

bcrypt:
strength: 10

Expand Down Expand Up @@ -325,3 +343,8 @@ environments:
cas:
appServerName: "https://auth.ala.org.au"

# Allow to disable some tools for non-ALA Portals
myProfile:
useDigiVol: true
useSandbox: true
useBiocollect: true
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import au.org.ala.ws.service.WebService
import grails.converters.JSON

import org.springframework.beans.factory.annotation.Qualifier
import org.passay.RuleResult
import org.springframework.context.i18n.LocaleContextHolder
import org.springframework.validation.Errors

/**
* Controller that handles the interactions with general public.
Expand All @@ -37,20 +40,24 @@ class RegistrationController {

def simpleCaptchaService
def emailService
def authService
def passwordService

@Qualifier('userService')
IUserService userService
def locationService
RecaptchaClient recaptchaClient
WebService webService
def messageSource

def index() {
redirect(action: 'createAccount')
}

def createAccount() {}
def createAccount() {
render(view: 'createAccount', model: [
passwordPolicy: passwordService.buildPasswordPolicy(),
])
}

def editAccount() {
def user = userService.currentUser
Expand All @@ -63,20 +70,26 @@ class RegistrationController {
render(view: 'accountError', model: [msg: "UserRecord not found with ID ${params.userId}"])
} else if (user.tempAuthKey == params.authKey) {
//keys match, so lets reset password
render(view: 'passwordReset', model: [user: user, authKey: params.authKey])
render(view: 'passwordReset', model: [user: user, authKey: params.authKey, passwordPolicy: passwordService.buildPasswordPolicy()])
} else {
render(view: 'authKeyExpired')
}
}

def updatePassword(UpdatePasswordCommand cmd) {
UserRecord user = userService.getUserById(cmd.userId as String)

// since the email address is the user name, use the part before the @ as the username
def username = user?.userName ?: user?.email ?: ''
def validationResult = passwordService.validatePassword(username, cmd?.password)
buildErrorMessages(validationResult, cmd.errors)

if (cmd.hasErrors()) {
render(view: 'passwordReset', model: [user: user, authKey: cmd.authKey, errors:cmd.errors, passwordMatchFail: true])
}
else {
withForm {
if (user.tempAuthKey == params.authKey) {
if (user.tempAuthKey == cmd.authKey) {
//update the password
try {
userService.resetPassword(user, cmd.password, true, null)
Expand Down Expand Up @@ -226,6 +239,13 @@ class RegistrationController {
// params.userName = params.email
}

def isCorrectPassword = passwordService.checkUserPassword(user, params.confirmUserPassword)
if (!isCorrectPassword) {
flash.message = 'Incorrect password. Could not update account details. Please try again.'
render(view: 'createAccount', model: [edit: true, user: user, props: user?.propsAsMap()])
return
}

def success = userService.updateUser(user.userId, params)

if (success) {
Expand All @@ -240,6 +260,8 @@ class RegistrationController {
}

def register() {
def paramsEmail = params?.email?.toString()
def paramsPassword = params?.password?.toString()
withForm {

def recaptchaKey = grailsApplication.config.getProperty('recaptcha.secretKey')
Expand All @@ -264,12 +286,20 @@ class RegistrationController {
}

//create user account...
if (!params.email || userService.isEmailRegistered(params.email)) {
def inactiveUser = !userService.isActive(params.email)
def lockedUser = userService.isLocked(params.email)
if (!paramsEmail || userService.isEmailRegistered(paramsEmail)) {
def inactiveUser = !userService.isActive(paramsEmail)
def lockedUser = userService.isLocked(paramsEmail)
render(view: 'createAccount', model: [edit: false, user: params, props: params, alreadyRegistered: true, inactiveUser: inactiveUser, lockedUser: lockedUser])
} else {

def passwordValidation = passwordService.validatePassword(paramsEmail, paramsPassword)
if (!passwordValidation.valid) {
log.warn("The password for user name '${paramsEmail}' did not meet the validation criteria '${passwordValidation}'")
flash.message = "The selected password does not meet the password policy. Please try again with a different password. ${buildErrorMessages(passwordValidation)}"
render(view: 'createAccount', model: [edit: false, user: params, props: params])
return
}

try {
//does a user with the supplied email address exist
def user = userService.registerUser(params)
Expand Down Expand Up @@ -364,4 +394,25 @@ class RegistrationController {
userService.enableMfa(params.userId, false)
redirect(action: 'editAccount')
}

private String buildErrorMessages(RuleResult validationResult, Errors errors = null) {
if (validationResult.valid) {
return null
}
def results = []
if (!validationResult.valid) {
def details = validationResult.details
for (def detail in details) {
for (String errorCode in detail.errorCodes) {
def fullErrorCode = "user.password.error.${errorCode?.toLowerCase()}"
def errorValues = detail.values as Object[]
if (errors) {
errors.rejectValue('password', fullErrorCode, errorValues, "Invalid password.")
}
results.add(messageSource.getMessage(fullErrorCode, errorValues, "Invalid password.", LocaleContextHolder.locale))
}
}
}
return results.unique().sort().join(' ')
}
}
49 changes: 48 additions & 1 deletion userdetails-plugin/grails-app/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ create.account.organisation=Organisation
create.account.country=Country
create.account.city=City
create.account.state.province=State / province
create.account.confirm.password=Please confirm your current password
create.account.update.account=Update account
create.account.disable.account=Disable account
create.account.btn=Create account
Expand Down Expand Up @@ -224,6 +225,50 @@ create.account.motivation.2=You don't have to set and remember yet another passw
create.account.motivation.3=Your account will be activated without going through verification emails
create.account.motivation.4=Overall you will save time
create.account.motivation.footer=Of course if you don't have an account with such providers or prefer to use a different email you still can create an account with us by filling in the information on the left.

account.password.policy.title=Password Policy
account.password.policy.requirements.length=A secure password will be at least {0} characters long.
account.password.policy.requirements.complexity.intro=The characters in it must satisfy at least {0} of the following {1} requirements:
account.password.policy.requirements.complexity.upper=upper case letter, e.g. A, B, C
account.password.policy.requirements.complexity.lower=lower case letter, e.g. a, b, c
account.password.policy.requirements.complexity.upperOrLower=upper or lower case letters
account.password.policy.requirements.complexity.number=numerical digit, e.g. 1, 2, 55
account.password.policy.requirements.complexity.special=special (non-alphanumeric) character, e.g. $, *, #, &
account.password.policy.requirements.complexity.common=The chosen password will be compared to a list of commonly-used passwords. \
If the password appears on the list you will need to choose a different password, as the password is too easy to guess.
account.password.policy.requirements.complexity.username=Your email cannot appear in the password.
account.password.policy.requirements.complexity.knownsequence=The password will be checked for groups of characters that are well-known and widely used. \
If these are found you will need to choose a different password, as the password is too easy to guess.

user.password.error.history_violation=Password matches one of {0} previous passwords.
user.password.error.illegal_word=The password is known to be commonly used, and is not secure ("{0}").
user.password.error.illegal_word_reversed=Password contains the reversed dictionary word "{0}".
user.password.error.illegal_digest_word=Password contains a dictionary word "{0}".
user.password.error.illegal_digest_word_reversed=Password contains a reversed dictionary word "{0}".
user.password.error.illegal_match=Password matches the illegal pattern "{0}".
user.password.error.allowed_match=Password must match pattern "{0}".
user.password.error.illegal_char=Password {1} the illegal character "{0}".
user.password.error.allowed_char=Password {1} the illegal character "{0}".
user.password.error.illegal_qwerty_sequence=Password contains a sequence of letters that is too easy to guess "{0}".
user.password.error.illegal_alphabetical_sequence=Password contains the illegal alphabetical sequence "{0}".
user.password.error.illegal_numerical_sequence=Password contains the illegal numerical sequence "{0}".
user.password.error.illegal_username=Password {1} the user id "{0}".
user.password.error.illegal_username_reversed=Password {1} the user id "{0}" in reverse.
user.password.error.illegal_whitespace=Password {1} a whitespace character "{0}".
user.password.error.illegal_number_range=Password {1} the number "{0}".
user.password.error.illegal_repeated_chars=Password contains {2} sequences of {0} or more repeated characters, but only {1} allowed: {3}.
user.password.error.insufficient_uppercase=Password must contain {0} or more uppercase characters, it currently contains {1}.
user.password.error.insufficient_lowercase=Password must contain {0} or more lowercase characters, it currently contains {1}.
user.password.error.insufficient_alphabetical=Password must contain {0} or more alphabetical characters, it currently contains {1}.
user.password.error.insufficient_digit=Password must contain {0} or more digit characters, it currently contains {1}.
user.password.error.insufficient_special=Password must contain {0} or more special characters, it currently contains {1}.
user.password.error.insufficient_characteristics=Password matches {0} of {2} character rules, but {1} are required.
user.password.error.insufficient_complexity=Password meets {1} complexity rules, but {2} are required ({0}).
user.password.error.insufficient_complexity_rules=No rules have been configured for a password of length {0}.
user.password.error.source_violation=Password cannot be the same as your {0} password.
user.password.error.too_long=Password must be no more than {1} characters in length ({0}).
user.password.error.too_short=Password must be {0} or more characters in length.
user.password.error.too_many_occurrences=Password contains {1} occurrences of the character "{0}", but at most {2} are allowed.
myprofile.your.alerts.desc=Update your alert settings for emails you receive from the Atlas
myprofile.update.desc=Update your email address, organisation and contact details
userdetails.index.reset.password.desc=Reset your Atlas password
Expand All @@ -245,6 +290,7 @@ myprofile.yourapikey=Your API key
#myprofile.yourapikey.desc2=You can generate an <b>API aecret</b> using the button below. Once its generated you will not be able to access this again so please keep it safe.
myprofile.yourapikey.desc1=This will can be used in your application to submit requests to the Atlas.
myprofile.yourapikey.desc2=This key should be sent as an HTTP header, along with <a href="https://en.wikipedia.org/wiki/JSON_Web_Token">JSON Web Token (JWT)</a>, giving you access additional services.

user.enabledMFA=MFA Enabled
user.enableMFA.title=Multi Factor Authentication
user.setupMFA=Setup MFA
Expand All @@ -258,4 +304,5 @@ updatePassword.constraint.1=at least 8 characters
updatePassword.constraint.2=a lower case letter
updatePassword.constraint.3=an upper case letter
updatePassword.constraint.4=a special character
updatePassword.constraint.5=a number
updatePassword.constraint.5=a number

Loading

0 comments on commit 1a62b43

Please sign in to comment.