diff --git a/scm-ui-components/packages/ui-components/src/forms/PasswordConfirmation.js b/scm-ui-components/packages/ui-components/src/forms/PasswordConfirmation.js index 3dc59ad906..b0f53cddeb 100644 --- a/scm-ui-components/packages/ui-components/src/forms/PasswordConfirmation.js +++ b/scm-ui-components/packages/ui-components/src/forms/PasswordConfirmation.js @@ -11,7 +11,7 @@ type State = { passwordConfirmationFailed: boolean }; type Props = { - passwordChanged: string => void, + passwordChanged: (string, boolean) => void, passwordValidator?: string => boolean, // Context props t: string => string @@ -98,14 +98,12 @@ class PasswordConfirmation extends React.Component { ); }; + isValid = () => { + return this.state.passwordValid && !this.state.passwordConfirmationFailed + }; + propagateChange = () => { - if ( - this.state.password && - this.state.passwordValid && - !this.state.passwordConfirmationFailed - ) { - this.props.passwordChanged(this.state.password); - } + this.props.passwordChanged(this.state.password, this.isValid()); }; } diff --git a/scm-ui/src/containers/ChangeUserPassword.js b/scm-ui/src/containers/ChangeUserPassword.js index 6fa38d470f..28a7af588a 100644 --- a/scm-ui/src/containers/ChangeUserPassword.js +++ b/scm-ui/src/containers/ChangeUserPassword.js @@ -21,7 +21,8 @@ type State = { password: string, loading: boolean, error?: Error, - passwordChanged: boolean + passwordChanged: boolean, + passwordValid: boolean }; class ChangeUserPassword extends React.Component { @@ -35,7 +36,8 @@ class ChangeUserPassword extends React.Component { passwordConfirmationError: false, validatePasswordError: false, validatePassword: "", - passwordChanged: false + passwordChanged: false, + passwordValid: false }; } @@ -83,6 +85,10 @@ class ChangeUserPassword extends React.Component { } }; + isValid = () => { + return this.state.oldPassword && this.state.passwordValid; + }; + render() { const { t } = this.props; const { loading, passwordChanged, error } = this.state; @@ -118,7 +124,7 @@ class ChangeUserPassword extends React.Component { key={this.state.passwordChanged ? "changed" : "unchanged"} /> @@ -126,8 +132,8 @@ class ChangeUserPassword extends React.Component { ); } - passwordChanged = (password: string) => { - this.setState({ ...this.state, password }); + passwordChanged = (password: string, passwordValid: boolean) => { + this.setState({ ...this.state, password, passwordValid: (!!password && passwordValid) }); }; onClose = () => { diff --git a/scm-ui/src/users/components/SetUserPassword.js b/scm-ui/src/users/components/SetUserPassword.js index 6c2c1ca25d..d318025f21 100644 --- a/scm-ui/src/users/components/SetUserPassword.js +++ b/scm-ui/src/users/components/SetUserPassword.js @@ -19,7 +19,8 @@ type State = { password: string, loading: boolean, error?: Error, - passwordChanged: boolean + passwordChanged: boolean, + passwordValid: boolean }; class SetUserPassword extends React.Component { @@ -32,7 +33,8 @@ class SetUserPassword extends React.Component { passwordConfirmationError: false, validatePasswordError: false, validatePassword: "", - passwordChanged: false + passwordChanged: false, + passwordValid: false }; } @@ -104,7 +106,7 @@ class SetUserPassword extends React.Component { key={this.state.passwordChanged ? "changed" : "unchanged"} /> @@ -112,8 +114,8 @@ class SetUserPassword extends React.Component { ); } - passwordChanged = (password: string) => { - this.setState({ ...this.state, password }); + passwordChanged = (password: string, passwordValid: boolean) => { + this.setState({ ...this.state, password, passwordValid: (!!password && passwordValid) }); }; onClose = () => { diff --git a/scm-ui/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js index 2003d22c89..bb9b0cbf41 100644 --- a/scm-ui/src/users/components/UserForm.js +++ b/scm-ui/src/users/components/UserForm.js @@ -22,7 +22,8 @@ type State = { user: User, mailValidationError: boolean, nameValidationError: boolean, - displayNameValidationError: boolean + displayNameValidationError: boolean, + passwordValid: boolean }; class UserForm extends React.Component { @@ -41,7 +42,8 @@ class UserForm extends React.Component { }, mailValidationError: false, displayNameValidationError: false, - nameValidationError: false + nameValidationError: false, + passwordValid: false }; } @@ -61,7 +63,6 @@ class UserForm extends React.Component { isValid = () => { const user = this.state.user; - const passwordValid = this.props.user ? !this.isFalsy(user.password) : true; return !( this.state.nameValidationError || this.state.mailValidationError || @@ -69,7 +70,7 @@ class UserForm extends React.Component { this.isFalsy(user.name) || this.isFalsy(user.displayName) || this.isFalsy(user.mail) || - passwordValid + !this.state.passwordValid ); }; @@ -166,9 +167,10 @@ class UserForm extends React.Component { }); }; - handlePasswordChange = (password: string) => { + handlePasswordChange = (password: string, passwordValid: boolean) => { this.setState({ - user: { ...this.state.user, password } + user: { ...this.state.user, password }, + passwordValid: !this.isFalsy(password) && passwordValid }); }; diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java b/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java index a369db66bd..f1f29bfe51 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SecureKeyResolver.java @@ -51,6 +51,7 @@ import static com.google.common.base.Preconditions.*; //~--- JDK imports ------------------------------------------------------------ import java.security.SecureRandom; +import java.util.Random; import javax.inject.Inject; import javax.inject.Singleton; @@ -88,12 +89,17 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter */ @Inject @SuppressWarnings("unchecked") - public SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory) + public SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory) { + this(storeFactory, new SecureRandom()); + } + + SecureKeyResolver(ConfigurationEntryStoreFactory storeFactory, Random random) { store = storeFactory .withType(SecureKey.class) .withName(STORE_NAME) .build(); + this.random = random; } //~--- methods -------------------------------------------------------------- @@ -112,7 +118,9 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter SecureKey key = store.get(subject); - checkState(key != null, "could not resolve key for subject %s", subject); + if (key == null) { + return getSecureKey(subject).getBytes(); + } return key.getBytes(); } @@ -161,7 +169,7 @@ public class SecureKeyResolver extends SigningKeyResolverAdapter //~--- fields --------------------------------------------------------------- /** secure randon */ - private final SecureRandom random = new SecureRandom(); + private final Random random; /** configuration entry store */ private final ConfigurationEntryStore store; diff --git a/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java b/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java index f85c0fbbbd..6747d40228 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/web/security/TokenRefreshFilter.java @@ -1,5 +1,6 @@ package sonia.scm.web.security; +import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,7 +65,13 @@ public class TokenRefreshFilter extends HttpFilter { } private void examineToken(HttpServletRequest request, HttpServletResponse response, BearerToken token) { - AccessToken accessToken = resolver.resolve(token); + AccessToken accessToken; + try { + accessToken = resolver.resolve(token); + } catch (AuthenticationException e) { + LOG.trace("could not resolve token", e); + return; + } if (accessToken instanceof JwtAccessToken) { refresher.refresh((JwtAccessToken) accessToken) .ifPresent(jwtAccessToken -> refreshToken(request, response, jwtAccessToken)); diff --git a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java index c4f281537e..cce3fea2b1 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/SecureKeyResolverTest.java @@ -44,12 +44,16 @@ import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.store.ConfigurationEntryStore; import sonia.scm.store.ConfigurationEntryStoreFactory; +import java.util.Random; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.in; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -99,10 +103,11 @@ public class SecureKeyResolverTest * Method description * */ - @Test(expected = IllegalStateException.class) + @Test public void testResolveSigningKeyBytesWithoutKey() { - resolver.resolveSigningKeyBytes(null, Jwts.claims().setSubject("test")); + byte[] bytes = resolver.resolveSigningKeyBytes(null, Jwts.claims().setSubject("test")); + assertThat(bytes[0]).isEqualTo((byte) 42); } /** @@ -132,7 +137,9 @@ public class SecureKeyResolverTest assertThat(storeParameters.getType()).isEqualTo(SecureKey.class); return true; }))).thenReturn(store); - resolver = new SecureKeyResolver(factory); + Random random = mock(Random.class); + doAnswer(invocation -> ((byte[]) invocation.getArguments()[0])[0] = 42).when(random).nextBytes(any()); + resolver = new SecureKeyResolver(factory, random); } //~--- fields ---------------------------------------------------------------