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 @@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
password-reset=Password reset
self.pwd.reset.success=Your password has been reset successfully!
self.pwd.reset.success.msg=An email has been sent to your address.
self.pwd.reset.success=Password reset process started
self.pwd.reset.success.msg=If an account matching the provided information exists, password reset instructions will be sent to the associated email address in a few seconds.
self.pwd.reset.error=Error during password reset!
self.pwd.reset.error.msg=Try again or contact an administrator.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
password-reset=Password reset
self.pwd.reset.success=Your password has been reset successfully!
self.pwd.reset.success.msg=An email has been sent to your address.
self.pwd.reset.success=Password reset process started
self.pwd.reset.success.msg=If an account matching the provided information exists, password reset instructions will be sent to the associated email address in a few seconds.
self.pwd.reset.error=Error during password reset!
self.pwd.reset.error.msg=Try again or contact an administrator.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
password-reset=Reset della password
self.pwd.reset.success=La password \u00e8 stata resettata con successo
self.pwd.reset.success.msg=Una email \u00e8 stata inviata all'indirizzo configurato
self.pwd.reset.success=Processo di reset password avviato
self.pwd.reset.success.msg=Se esiste un account corrispondente alle informazioni fornite, le istruzioni per il reset della password verranno inviate all'indirizzo email associato entro pochi secondi.
self.pwd.reset.error=Error during password reset!
self.pwd.reset.error.msg=Try again or contact an administrator.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
password-reset=\u30d1\u30b9\u30ef\u30fc\u30c9\u306e\u30ea\u30bb\u30c3\u30c8
self.pwd.reset.success=\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u30ea\u30bb\u30c3\u30c8\u3057\u307e\u3057\u305f
self.pwd.reset.success.msg=An email has been sent to your address.
self.pwd.reset.success=Password reset process started
self.pwd.reset.success.msg=If an account matching the provided information exists, password reset instructions will be sent to the associated email address in a few seconds.
self.pwd.reset.error=Error during password reset!
self.pwd.reset.error.msg=Try again or contact an administrator.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
password-reset=Resetar a senha
self.pwd.reset.success=Senha redefinida com sucesso
self.pwd.reset.success.msg=An email has been sent to your address.
self.pwd.reset.success=Password reset process started
self.pwd.reset.success.msg=If an account matching the provided information exists, password reset instructions will be sent to the associated email address in a few seconds.
self.pwd.reset.error=Error during password reset!
self.pwd.reset.error.msg=Try again or contact an administrator.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
password-reset=\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f
self.pwd.reset.success=\u041f\u0430\u0440\u043e\u043b\u044c \u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0431\u0440\u043e\u0448\u0435\u043d
self.pwd.reset.success.msg=An email has been sent to your address.
self.pwd.reset.success=Password reset process started
self.pwd.reset.success.msg=If an account matching the provided information exists, password reset instructions will be sent to the associated email address in a few seconds.
self.pwd.reset.error=Error during password reset!
self.pwd.reset.error.msg=Try again or contact an administrator.
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,8 @@ public UserSelfLogic userSelfLogic(
final DelegationDAO delegationDAO,
final AccessTokenDAO accessTokenDAO,
final ExternalResourceDAO resourceDAO,
final RuleProvider ruleProvider) {
final RuleProvider ruleProvider,
final SecurityProperties securityProperties) {

return new UserSelfLogic(
realmSearchDAO,
Expand All @@ -537,7 +538,8 @@ public UserSelfLogic userSelfLogic(
delegationDAO,
accessTokenDAO,
resourceDAO,
ruleProvider);
ruleProvider,
securityProperties);
}

@ConditionalOnMissingBean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import org.apache.syncope.core.spring.policy.AccountPolicyException;
import org.apache.syncope.core.spring.policy.PasswordPolicyException;
import org.apache.syncope.core.spring.security.AuthContextUtils;
import org.apache.syncope.core.spring.security.SecurityProperties;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -85,6 +86,8 @@ protected static void throwMfaWasSet(final String username) {

protected final RuleProvider ruleProvider;

protected final SecurityProperties securityProperties;

public UserSelfLogic(
final RealmSearchDAO realmSearchDAO,
final AnyTypeDAO anyTypeDAO,
Expand All @@ -97,7 +100,8 @@ public UserSelfLogic(
final DelegationDAO delegationDAO,
final AccessTokenDAO accessTokenDAO,
final ExternalResourceDAO resourceDAO,
final RuleProvider ruleProvider) {
final RuleProvider ruleProvider,
final SecurityProperties securityProperties) {

super(realmSearchDAO,
anyTypeDAO,
Expand All @@ -111,6 +115,7 @@ public UserSelfLogic(
this.accessTokenDAO = accessTokenDAO;
this.resourceDAO = resourceDAO;
this.ruleProvider = ruleProvider;
this.securityProperties = securityProperties;
}

@PreAuthorize("isAuthenticated() "
Expand Down Expand Up @@ -274,17 +279,27 @@ public void requestPasswordReset(final String username, final String securityAns
throw sce;
}

String key = userDAO.findKey(username).
orElseThrow(() -> new NotFoundException("User " + username));
Optional<String> key = userDAO.findKey(username);
if (key.isEmpty()) {
if (!securityProperties.getPasswordReset().isHideDetails()) {
throw new NotFoundException("User " + username);
}
LOG.warn("Ignoring password reset request for unknown user");
return;
}

if (confParamOps.get(
AuthContextUtils.getDomain(), StandardConfParams.PASSWORD_RESET_SECURITY_QUESTION, false, boolean.class)
&& (securityAnswer == null || !provisioningManager.checkSecurityAnswer(key, securityAnswer))) {
&& (securityAnswer == null || !provisioningManager.checkSecurityAnswer(key.get(), securityAnswer))) {

throw SyncopeClientException.build(ClientExceptionType.InvalidSecurityAnswer);
if (!securityProperties.getPasswordReset().isHideDetails()) {
throw SyncopeClientException.build(ClientExceptionType.InvalidSecurityAnswer);
}
LOG.warn("Ignoring password reset request with missing or invalid security answer");
return;
}

provisioningManager.requestPasswordReset(key, AuthContextUtils.getUsername(), REST_CONTEXT);
provisioningManager.requestPasswordReset(key.get(), AuthContextUtils.getUsername(), REST_CONTEXT);
}

@PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
Expand All @@ -298,7 +313,9 @@ public void confirmPasswordReset(final String token, final String password) {
}

String key = userDAO.findByToken(token).
orElseThrow(() -> new NotFoundException("User with token " + token));
orElseThrow(() -> new NotFoundException(securityProperties.getPasswordReset().isHideDetails()
? "Invalid password reset token"
: "User with token " + token));

provisioningManager.confirmPasswordReset(
key, token, password, AuthContextUtils.getUsername(), REST_CONTEXT);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.syncope.core.logic;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Optional;
import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
import org.apache.syncope.common.keymaster.client.api.StandardConfParams;
import org.apache.syncope.common.lib.SyncopeClientException;
import org.apache.syncope.common.lib.types.ClientExceptionType;
import org.apache.syncope.core.persistence.api.EncryptorManager;
import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
import org.apache.syncope.core.persistence.api.dao.UserDAO;
import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
import org.apache.syncope.core.provisioning.api.rules.RuleProvider;
import org.apache.syncope.core.spring.security.SecurityProperties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class UserSelfLogicPasswordResetTest {

private ConfParamOps confParamOps;

private UserDAO userDAO;

private UserProvisioningManager provisioningManager;

private SecurityProperties securityProperties;

private UserSelfLogic logic;

@BeforeEach
public void setUp() {
confParamOps = mock(ConfParamOps.class);
userDAO = mock(UserDAO.class);
provisioningManager = mock(UserProvisioningManager.class);
securityProperties = new SecurityProperties();

when(confParamOps.get(any(), eq(StandardConfParams.PASSWORD_RESET_ALLOWED), eq(false), eq(boolean.class))).
thenReturn(true);
when(confParamOps.get(
any(), eq(StandardConfParams.PASSWORD_RESET_SECURITY_QUESTION), eq(false), eq(boolean.class))).
thenReturn(true);

logic = new UserSelfLogic(
mock(RealmSearchDAO.class),
mock(AnyTypeDAO.class),
mock(TemplateUtils.class),
userDAO,
mock(UserDataBinder.class),
provisioningManager,
mock(EncryptorManager.class),
confParamOps,
mock(DelegationDAO.class),
mock(AccessTokenDAO.class),
mock(ExternalResourceDAO.class),
mock(RuleProvider.class),
securityProperties);
}

@Test
public void defaultPasswordResetHidesUnknownUser() {
when(userDAO.findKey("missing")).thenReturn(Optional.empty());

assertDoesNotThrow(() -> logic.requestPasswordReset("missing", "answer"));
verify(provisioningManager, never()).requestPasswordReset(anyString(), anyString(), anyString());
}

@Test
public void passwordResetDetailsCanBeExposedForCompatibility() {
securityProperties.getPasswordReset().setHideDetails(false);
when(userDAO.findKey("missing")).thenReturn(Optional.empty());

NotFoundException e = assertThrows(
NotFoundException.class,
() -> logic.requestPasswordReset("missing", "answer"));
assertTrue(e.getMessage().contains("missing"));
}

@Test
public void defaultPasswordResetHidesInvalidSecurityAnswer() {
when(userDAO.findKey("rossini")).thenReturn(Optional.of("user-key"));
when(provisioningManager.checkSecurityAnswer("user-key", "wrong")).thenReturn(false);

assertDoesNotThrow(() -> logic.requestPasswordReset("rossini", "wrong"));
verify(provisioningManager, never()).requestPasswordReset(anyString(), anyString(), anyString());
}

@Test
public void invalidSecurityAnswerDetailsCanBeExposedForCompatibility() {
securityProperties.getPasswordReset().setHideDetails(false);
when(userDAO.findKey("rossini")).thenReturn(Optional.of("user-key"));
when(provisioningManager.checkSecurityAnswer("user-key", "wrong")).thenReturn(false);

SyncopeClientException e = assertThrows(
SyncopeClientException.class,
() -> logic.requestPasswordReset("rossini", "wrong"));
assertEquals(ClientExceptionType.InvalidSecurityAnswer, e.getType());
}

@Test
public void defaultPasswordResetDoesNotReflectInvalidToken() {
when(userDAO.findByToken("WRONG TOKEN")).thenReturn(Optional.empty());

NotFoundException e = assertThrows(
NotFoundException.class,
() -> logic.confirmPasswordReset("WRONG TOKEN", "password"));
assertFalse(e.getMessage().contains("WRONG TOKEN"));
}

@Test
public void invalidTokenDetailsCanBeExposedForCompatibility() {
securityProperties.getPasswordReset().setHideDetails(false);
when(userDAO.findByToken("WRONG TOKEN")).thenReturn(Optional.empty());

NotFoundException e = assertThrows(
NotFoundException.class,
() -> logic.confirmPasswordReset("WRONG TOKEN", "password"));
assertTrue(e.getMessage().contains("WRONG TOKEN"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ public void setLockSeconds(final long lockSeconds) {
}
}

public static class PasswordResetProperties {

private boolean hideDetails = true;

public boolean isHideDetails() {
return hideDetails;
}

public void setHideDetails(final boolean hideDetails) {
this.hideDetails = hideDetails;
}
}

public static class DigesterProperties {

private int saltIterations = 1;
Expand Down Expand Up @@ -149,6 +162,8 @@ public void setUseLenientSaltSizeCheck(final boolean useLenientSaltSizeCheck) {

private final AuthenticationThrottleProperties authenticationThrottle = new AuthenticationThrottleProperties();

private final PasswordResetProperties passwordReset = new PasswordResetProperties();

private final DigesterProperties digester = new DigesterProperties();

public String getAdminUser() {
Expand Down Expand Up @@ -243,6 +258,10 @@ public AuthenticationThrottleProperties getAuthenticationThrottle() {
return authenticationThrottle;
}

public PasswordResetProperties getPasswordReset() {
return passwordReset;
}

public DigesterProperties getDigester() {
return digester;
}
Expand Down
2 changes: 2 additions & 0 deletions core/starter/src/main/resources/core.properties
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ security.authenticationThrottle.maxAttempts=5
security.authenticationThrottle.windowSeconds=60
security.authenticationThrottle.lockSeconds=60

security.passwordReset.hideDetails=true

# default for LDAP / RFC2307 SSHA
security.digester.saltIterations=1
security.digester.saltSizeBytes=8
Expand Down
Loading
Loading