1. Introduction
Yeah, same as mobile-first. Main goal of that project is learn spring-security topic from basis to advanced and share with people. Because no one enterprise application can’t go live without security, I believe it should be done first. It’s also can help you avoid situation when application architecture needs to be refactored to have possibility to apply security to it… I saw that many times… For some reason, big part of software developers community do not care about security from beginning or even to the end. I think main reason is because security hard topic. And it’s really sad - many developers want doing it right, but for some reasons, people teaching them develop software missing security.
Whole purpose of that project is learn and share such important topic in software development world. Any enterprise project cannot go live without security. So my personal lifestyle is always think about security first, and only after build any other application functionality. Any good framework must provide developers some good security solution …and spring does. Spring Soot is amazing framework. It’s really one of the best java framework I know. As part of it, Spring Security provide us a lot of great features - it’s easiest security solutions I have ever seen and used in java.
2. JDBC
2.1. authorization server
Let’s start with building jdbc-oauth2-auth-server
./gradlew clean :apps:jd-o-a-s:bootRun
http -a clientId:secret \
--form post :8001/oauth/token \
grant_type=password \
username=usr \
password=pwd
# http -a clientId:secret --form post :8001/oauth/token grant_type=password username=usr password=pwd
{
"access_token": "aa3d78ea-ac9a-4a37-9b8c-3d31b6abfe15",
"expires_in": 43199,
"refresh_token": "09ae7996-719f-4e24-9389-469f90761853",
"scope": "read",
"token_type": "bearer"
}
http -a clientId:secret \
--form post :8001/oauth/check_token \
grant_type=implicit \
token=aa3d78ea-ac9a-4a37-9b8c-3d31b6abfe15
# http -a clientId:secret --form post :8001/oauth/check_token grant_type=implicit token=aa3d78ea-ac9a-4a37-9b8c-3d31b6abfe15
{
"active": true,
"authorities": [
"ROLE_ADMIN",
"ADMIN",
"USER",
"ROLE_USER"
],
"client_id": "passwordClientId",
"exp": 1528017060,
"scope": [
"read"
],
"user_name": "usr"
}
2.2. implementation
-
provide UserDetailsService (mocked in our case for simplicity:
JdbcUserDetailsService
) -
after you have UserDetailsService, you can create
AuthenticationManagerConfig
-
finally you can configure
JdbcOauth2AuthServerConfig
-
and of course, do not forget about password encoder:
PasswordEncoderConfig
2.2.1. provide database
spring:
datasource:
url: jdbc:h2:file:./security-first;AUTO_SERVER=TRUE;MULTI_THREADED=TRUE;MODE=MYSQL;DB_CLOSE_ON_EXIT=FALSE;AUTO_RECONNECT=TRUE
username: security-first
password: security-first
driver-class-name: org.h2.Driver
hikari.connection-test-query: 'SELECT 1;'
h2.console.enabled: true
logging.level.org.springframework.jdbc: 'DEBUG'
@Bean
public DataSourceInitializer dataSourceInitializer(final DataSource dataSource) {
final DataSourceInitializer initializer = new DataSourceInitializer();
initializer.setDataSource(dataSource);
initializer.setDatabasePopulator(databasePopulator());
return initializer;
}
private DatabasePopulator databasePopulator() {
final ClassPathResource schema = new ClassPathResource("/schema-h2.sql", DataSourceConfig.class.getClassLoader());
return new ResourceDatabasePopulator(false, true, UTF_8.displayName(), schema);
}
drop table if exists oauth_client_details;
create table oauth_client_details (
client_id VARCHAR(255) PRIMARY KEY,
resource_ids VARCHAR(255),
client_secret VARCHAR(255),
scope VARCHAR(255),
authorized_grant_types VARCHAR(255),
web_server_redirect_uri VARCHAR(255),
authorities VARCHAR(255),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(255)
);
drop table if exists oauth_client_token;
create table oauth_client_token (
token_id VARCHAR(255),
token LONGVARBINARY,
authentication_id VARCHAR(255) PRIMARY KEY,
user_name VARCHAR(255),
client_id VARCHAR(255)
);
drop table if exists oauth_access_token;
create table oauth_access_token (
token_id VARCHAR(255),
token LONGVARBINARY,
authentication_id VARCHAR(255) PRIMARY KEY,
user_name VARCHAR(255),
client_id VARCHAR(255),
authentication LONGVARBINARY,
refresh_token VARCHAR(255)
);
drop table if exists oauth_refresh_token;
create table oauth_refresh_token (
token_id VARCHAR(255),
token LONGVARBINARY,
authentication LONGVARBINARY
);
drop table if exists oauth_code;
create table oauth_code (
code VARCHAR(255), authentication LONGVARBINARY
);
drop table if exists oauth_approvals;
create table oauth_approvals (
userId VARCHAR(255),
clientId VARCHAR(255),
scope VARCHAR(255),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);
drop table if exists ClientDetails;
create table ClientDetails (
appId VARCHAR(255) PRIMARY KEY,
resourceIds VARCHAR(255),
appSecret VARCHAR(255),
scope VARCHAR(255),
grantTypes VARCHAR(255),
redirectUrl VARCHAR(255),
authorities VARCHAR(255),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(255)
);
2.2.2. jdbc user details service
@Service
@RequiredArgsConstructor
class JdbcUserDetailsService implements UserDetailsService {
final PasswordEncoder encoder;
final JdbcTemplate jdbcTemplate;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return User
.withUsername("usr")
//.password("pwd")
.password(null == encoder ? "{noop}pwd" : encoder.encode("pwd"))
.disabled(false)
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.authorities("USER", "ADMIN", "ROLE_USER", "ROLE_ADMIN")
.build();
}
}
2.2.3. authentication manager config
@Configuration
@RequiredArgsConstructor
class AuthenticationManagerConfig extends WebSecurityConfigurerAdapter {
final JdbcUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
2.2.4. jdbc oauth2 auth server config
/**
* CORS Filter.
*
* https://github.com/spring-projects/spring-security-oauth/issues/938#issuecomment-286243307
* @Orger(2)
* +
* @Bean FilterRegistrationBean corsFilter() { ... }
*/
@Order(2)
@Configuration
@RequiredArgsConstructor
@EnableAuthorizationServer
class JdbcOauth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {
final DataSource dataSource;
final PasswordEncoder encoder;
final ClientsProps clientsProps;
final AuthenticationManager authenticationManager;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
final Client resourceAppClient = clientsProps.getResourceAppClient();
final Client passwordClient = clientsProps.getPasswordClient();
clients
.jdbc(dataSource)
.withClient(resourceAppClient.getClientId())
.authorizedGrantTypes(resourceAppClient.getAuthorizedGrantTypes())
.secret(resourceAppClient.generateSecret(encoder))
.scopes(resourceAppClient.getScopes())
.autoApprove(resourceAppClient.isAutoApprove())
.and()
.withClient(passwordClient.getClientId())
.authorizedGrantTypes(passwordClient.getAuthorizedGrantTypes())
.secret(passwordClient.generateSecret(encoder))
.scopes(passwordClient.getScopes())
.autoApprove(passwordClient.isAutoApprove())
;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore())
.authenticationManager(authenticationManager)
;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Bean
public FilterRegistrationBean corsFilter() {
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
final CorsFilter corsFilter = new CorsFilter(source);
final FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
2.3. resource server
Now we ready to go with building jdbc-oauth2-resource-server
./gradlew :apps:oauth2-jdbc:resource-server:bootRun
2.4. implementation
-
implement resource server
/check_token
remote token service endpoint:RemoteTokenServicesConfig
2.4.1. remote token services
final AppProps appProps;
final ClientsProps clientsProps;
@Bean
@Primary
public RemoteTokenServices tokenService() {
final String checkTokenEndpointUrl = format("%s/oauth/check_token", appProps.getAuthServerUrl());
final RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
final ClientsProps.Client client = clientsProps.getResourceAppClient();
tokenService.setClientId(client.getClientId());
tokenService.setClientSecret(client.getSecret());
return tokenService;
}
2.5. testing
Let’s test how clients can access resource-server resources using obtained token from auth-server….
http -a clientId:secret --form post :8001/oauth/token grant_type=password username=usr password=pwd
{
"access_token": "be5caf90-f197-43bf-86e2-cd4560066871",
"expires_in": 40917,
"refresh_token": "4b2991af-6e1b-40cc-ab83-3f0c135d8c12",
"scope": "read",
"token_type": "bearer"
}
Now client can use received access_token value to query resource-server /
http :8002/
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
http :8002/ Authorization:'Bearer be5caf90-f197-43bf-86e2-cd4560066871'
{
"at": "2018-06-03T01:20:45.153Z",
"ololo": "trololo"
}
Done!
2.6. resource server
Now we ready to go with building jdbc-oauth2-resource-server
./gradlew clean :apps:jd-o-r-s:bootRun
2.7. implementation
-
implement resource server
/check_token
remote token service endpoint:RemoteTokenServicesConfig