Commit 56892e2b authored by Matija Obreza's avatar Matija Obreza

Merge branch '30-oauth2-client-secret' into 'master'

Resolve "Encoded OAuth client secret"

Closes #30

See merge request genesys-pgr/application-blocks!32
parents d1bdd379 0cac90e9
/*
* Copyright 2017 Global Crop Diversity Trust
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -17,6 +17,8 @@ package org.genesys.blocks.oauth.persistence;
import org.genesys.blocks.oauth.model.OAuthClient;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.stereotype.Repository;
......@@ -43,4 +45,14 @@ public interface OAuthClientRepository extends JpaRepository<OAuthClient, Long>,
*/
OAuthClient findByIdAndVersion(long id, int version);
/**
* Sets the client secret.
*
* @param id the oauth client database ID
* @param encodedPassword the encoded password
*/
@Modifying
@Query("update OAuthClient set clientSecret = ?2 where id = ?1")
void setClientSecret(long id, String encodedPassword);
}
......@@ -89,4 +89,12 @@ public interface OAuthClientDetailsService extends ClientDetailsService {
*/
List<OAuthClient> autocompleteClients(String title, int limit);
/**
* Generates a new clientSecret
*
* @param oauthClient the client to regenerate secret for
* @return the new cliet secret
*/
String resetSecret(OAuthClient oauthClient);
}
......@@ -24,8 +24,6 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import com.querydsl.core.types.Predicate;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.oauth.model.AccessToken;
......@@ -44,6 +42,8 @@ import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
......@@ -57,6 +57,8 @@ import org.springframework.security.oauth2.provider.token.DefaultAuthenticationK
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.querydsl.core.types.Predicate;
/**
* The Class OAuthServiceImpl.
*/
......@@ -83,6 +85,9 @@ public class OAuthServiceImpl implements OAuthClientDetailsService, OAuthTokenSt
@Autowired
private AccessTokenRepository accessTokenRepository;
@Autowired
public PasswordEncoder passwordEncoder;
/** The authentication key generator. */
private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
......@@ -630,7 +635,7 @@ public class OAuthServiceImpl implements OAuthClientDetailsService, OAuthTokenSt
client.setAccessTokenValidity(accessTokenValidity);
client.setRefreshTokenValidity(refreshTokenValidity);
client.setClientId(clientId);
client.setClientSecret(clientSecret);
client.setClientSecret(passwordEncoder.encode(clientSecret));
client.getScope().add("read");
client.getScope().add("write");
client.getAuthorizedGrantTypes().add("authorization_code");
......@@ -655,7 +660,7 @@ public class OAuthServiceImpl implements OAuthClientDetailsService, OAuthTokenSt
final OAuthClient newClient = new OAuthClient();
newClient.apply(client);
newClient.setClientId(clientId);
newClient.setClientSecret(clientSecret);
newClient.setClientSecret(passwordEncoder.encode(clientSecret));
return lazyLoad(oauthClientRepository.save(newClient));
}
......@@ -686,7 +691,26 @@ public class OAuthServiceImpl implements OAuthClientDetailsService, OAuthTokenSt
.or(QOAuthClient.oAuthClient.clientId.startsWithIgnoreCase(term))
// description contains
.or(QOAuthClient.oAuthClient.description.contains(term));
return oauthClientRepository.findAll(predicate, new PageRequest(0, Math.min(100, limit), new Sort("title"))).getContent();
}
@Override
@Transactional
@PreAuthorize("hasRole('ADMINISTRATOR') or hasPermission(#oauthClient, 'ADMINISTRATION')")
public final String resetSecret(OAuthClient oauthClient) {
oauthClient = oauthClientRepository.findOne(oauthClient.getId());
String oldHash = oauthClient.getClientSecret();
String newHash = null;
String clientSecret = null;
do {
clientSecret = RandomStringUtils.randomAlphanumeric(32);
newHash = passwordEncoder.encode(clientSecret);
} while (oldHash != null && oldHash.equals(newHash));
oauthClient.setClientSecret(newHash);
oauthClient = oauthClientRepository.save(oauthClient);
return clientSecret;
}
}
/*
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed 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.genesys.blocks.oauth.util;
import org.genesys.blocks.oauth.model.OAuthClient;
import org.genesys.blocks.oauth.persistence.OAuthClientRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
/**
* App Blocks utility with handy updates :-)
*
* @author Matija Obreza
*/
@Component
public class AppBlocksInitializer implements InitializingBean {
private static final Logger LOG = LoggerFactory.getLogger(AppBlocksInitializer.class);
@Autowired
private OAuthClientRepository oauthClientRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
@Qualifier("transactionManager")
protected PlatformTransactionManager txManager;
public void afterPropertiesSet() throws Exception {
TransactionTemplate tmpl = new TransactionTemplate(txManager);
tmpl.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
encodePasswords();
}
});
}
/**
* Ensure that all clientSecrets are encrypted
*/
void encodePasswords() {
long count = 0;
for (OAuthClient oauthClient : oauthClientRepository.findAll()) {
if (!oauthClient.getClientSecret().startsWith("$2a$")) {
count++;
LOG.warn("Migrating plain-text client secret to bcrypt for {}", oauthClient.getClientId());
oauthClientRepository.setClientSecret(oauthClient.getId(), passwordEncoder.encode(oauthClient.getClientSecret()));
}
}
if (count == 0) {
LOG.warn("\n\n\t** All OAuth Client secrets are encoded **\n\t You can remove the AppBlocksInitializer.\n\n");
}
}
}
......@@ -82,7 +82,7 @@ public class AccountLockoutManager {
}
/**
* Reset failed attempt statistics on successful login.
* Reset failed attempt statistics on successful login and update the last login date.
*
* @param userName the user name
*/
......@@ -93,6 +93,12 @@ public class AccountLockoutManager {
loginAttempts.remove(userName);
LOG.info("Successful login. Removed failed login statistics for " + userName + " " + stats);
}
try {
userService.updateLastLogin(userName);
} catch (final NoUserFoundException e) {
LOG.warn(e.getMessage());
}
}
/**
......
......@@ -39,7 +39,7 @@ public class AuthenticationSuccessListener implements ApplicationListener<Authen
/** The lockout manager. */
@Autowired
private AccountLockoutManager lockoutManager;
/*
* (non-Javadoc)
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.
......
/*
* Copyright 2017 Global Crop Diversity Trust
* Copyright 2018 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......@@ -74,7 +74,9 @@ public abstract class BasicUser<R extends GrantedAuthority> extends AclSid imple
/** The google. */
GOOGLE,
/** The system. */
SYSTEM
SYSTEM,
/** Deleted user accounts */
DELETED
}
/** The uuid. */
......@@ -130,6 +132,12 @@ public abstract class BasicUser<R extends GrantedAuthority> extends AclSid imple
@Column(length = 20)
private AccountType accountType = AccountType.LOCAL;
/** The date of last successful login. */
@JsonView(JsonViews.Internal.class)
@Temporal(TemporalType.TIMESTAMP)
private Date lastLogin;
public BasicUser() {
setPrincipal(true);
}
......@@ -414,4 +422,21 @@ public abstract class BasicUser<R extends GrantedAuthority> extends AclSid imple
this.accountType = accountType;
}
/**
* Gets the last login.
*
* @return the last login
*/
public Date getLastLogin() {
return lastLogin;
}
/**
* Sets the last login.
*
* @param lastLogin the new last login
*/
public void setLastLogin(Date lastLogin) {
this.lastLogin = lastLogin;
}
}
......@@ -157,4 +157,12 @@ public interface BasicUserService<R extends GrantedAuthority, T extends BasicUse
*/
T setAccountType(T user, AccountType accountType);
/**
* Update user's last login date
*
* @param userName
* @throws NoUserFoundException if username not found in the system
*/
void updateLastLogin(String userName) throws NoUserFoundException;
}
......@@ -15,6 +15,7 @@
*/
package org.genesys.blocks.security.service.impl;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
......@@ -325,4 +326,18 @@ public abstract class BasicUserServiceImpl<R extends GrantedAuthority, T extends
return _repository.save(u);
}
@Override
@Transactional
public void updateLastLogin(String userName) throws NoUserFoundException {
T u = getUserByEmail(userName);
u.setLastLogin(new Date());
// Set account to expire 1 year after last login
Calendar accountExpires = Calendar.getInstance();
accountExpires.add(Calendar.YEAR, 1);
u.setAccountExpires(accountExpires.getTime());
_repository.save(u);
}
}
......@@ -21,11 +21,6 @@ import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.genesys.blocks.oauth.model.OAuthClient;
import org.genesys.blocks.oauth.model.OAuthRole;
import org.genesys.blocks.security.rest.AbstractRestTest;
......@@ -33,6 +28,7 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.web.servlet.MockMvc;
......@@ -40,6 +36,11 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.Base64Utils;
import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* @author Maxym Borodenko
*/
......@@ -54,6 +55,9 @@ public class OAuth2GrantTypeTest extends AbstractRestTest {
private static final ObjectMapper objectMapper;
@Autowired
public PasswordEncoder passwordEncoder;
static {
objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
......@@ -84,9 +88,18 @@ public class OAuth2GrantTypeTest extends AbstractRestTest {
}
private OAuth2AccessToken getAccessToken() throws Exception {
final MockHttpServletResponse response = mockMvc.perform(post("/oauth/token").header("Authorization", "Basic " + new String(Base64Utils.encode((DEFAULT_CLIENT_ID + ":"
+ DEFAULT_CLIENT_SECRET).getBytes()))).param("grant_type", "client_credentials").param("client_id", DEFAULT_CLIENT_ID).param("scope", "read")).andReturn().getResponse();
final MockHttpServletResponse response = mockMvc
/*@formatter:off*/
.perform(post("/oauth/token")
.header("Authorization",
"Basic " + new String(Base64Utils.encode((DEFAULT_CLIENT_ID + ":" + DEFAULT_CLIENT_SECRET).getBytes())))
.param("grant_type", "client_credentials")
.param("client_id", DEFAULT_CLIENT_ID)
.param("scope", "read"))
// .andDo(MockMvcResultHandlers.print())
.andReturn()
.getResponse();
/*@formatter:on*/
return objectMapper.readValue(response.getContentAsByteArray(), OAuth2AccessToken.class);
}
......
......@@ -15,11 +15,8 @@
*/
package org.genesys.blocks.oauth;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;
import org.genesys.blocks.oauth.model.OAuthClient;
......@@ -38,10 +35,11 @@ public class OAuthClientTest extends AbstractRestTest {
@Test
public void createClient() {
OAuthClient client = oauthClientDetailsService.addClient("JUnit OAuth Client", null, null, null, null);
OAuthClient client = oauthClientDetailsService.addClient(makeClient());
assertThat("OAuthClient#id must be generated", client.getId(), not(nullValue()));
assertThat("OAuthClient#clientId must be generated", client.getClientId(), not(nullValue()));
assertThat("OAuthClient#clientSecret must be generated", client.getClientSecret(), not(nullValue()));
assertThat("OAuthClient#clientSecret must be hashed", client.getClientSecret(), startsWith("$2a$"));
final OAuthClient storedClient = oAuthClientRepository.findOne(client.getId());
assertThat("Could not load persisted OAuthClient", storedClient, not(nullValue()));
......@@ -53,7 +51,7 @@ public class OAuthClientTest extends AbstractRestTest {
@Test
public void updateClient() {
OAuthClient client = oauthClientDetailsService.addClient("JUnit OAuth Client", null, null, null, null);
OAuthClient client = oauthClientDetailsService.addClient(makeClient());
client.setTitle("AAAABBBBCCC");
OAuthClient updatedClient = oauthClientDetailsService.updateClient(client.getId(), client.getVersion(), client);
......@@ -65,4 +63,21 @@ public class OAuthClientTest extends AbstractRestTest {
assertThat("Autocomplete must return the 1 client", oauthClientDetailsService.autocompleteClients(updatedClient.getTitle().substring(0, 10), 10), hasSize(1));
assertThat("Autocomplete must return the 1 client", oauthClientDetailsService.autocompleteClients(updatedClient.getTitle().substring(0, 5), 10), hasSize(1));
}
@Test
public void resetSecret() {
OAuthClient client = oauthClientDetailsService.addClient(makeClient());
String oldSecret = client.getClientSecret();
String newSecret = oauthClientDetailsService.resetSecret(client);
assertThat("OAuthClient#clientSecret must not be updated", newSecret, not(is(oldSecret)));
}
private OAuthClient makeClient() {
OAuthClient client=new OAuthClient();
client.setTitle("JUnit OAuth Client");
client.setDescription("Haha");
return client;
}
}
......@@ -20,6 +20,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author Maxym Borodenko
......@@ -33,4 +35,9 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment