Spring Security: Difference between revisions
(7 intermediate revisions by the same user not shown) | |||
Line 155: | Line 155: | ||
==WebSecurityConfigurerAdapter== | ==WebSecurityConfigurerAdapter== | ||
The WebSecurityConfigurerAdapter is a key class to configuring how spring behaves. I hope to go through and list the key points in this section.<br> | The WebSecurityConfigurerAdapter is a key class to configuring how spring behaves. I hope to go through and list the key points in this section.<br> | ||
===Example Implementation=== | |||
Here is an example of the entire configure method. We will go through and explain the parts as we go. | |||
<syntaxhighlight lang="java"> | |||
public class ConferenceSecurityConfig extends WebSecurityConfigurerAdapter { | |||
@Autowired | |||
private DataSource dataSource; | |||
@Autowired | |||
private ConferenceUserDetailsContextMapper ctxMapper; | |||
@Override | |||
protected void configure(final HttpSecurity http) throws Exception { | |||
http | |||
.authorizeRequests() | |||
//.antMatchers("/admin/**").hasRole("ADMIN") | |||
.antMatchers("/anonymous*").anonymous() | |||
.antMatchers("/login*").permitAll() | |||
.antMatchers("/account*").permitAll() | |||
.antMatchers("/password*").permitAll() | |||
.antMatchers("/assets/css/**", "assets/js/**", "/images/**").permitAll() | |||
.antMatchers("/index*").permitAll() | |||
.anyRequest().authenticated() | |||
.and() | |||
.formLogin() | |||
.loginPage("/login") | |||
.loginProcessingUrl("/perform_login") | |||
.failureUrl("/login?error=true") | |||
.permitAll() | |||
.defaultSuccessUrl("/", true) | |||
.and() | |||
.rememberMe() | |||
.key("superSecretKey") | |||
.tokenRepository(tokenRepository()) | |||
.and() | |||
.logout() | |||
.logoutSuccessUrl("/login?logout=true") | |||
.logoutRequestMatcher(new AntPathRequestMatcher("/perform_logout", "GET")) | |||
.invalidateHttpSession(true) | |||
.deleteCookies("JSESSIONID") | |||
.permitAll(); | |||
} | |||
</syntaxhighlight> | |||
===Method Security=== | ===Method Security=== | ||
Firstly, these annotations are for method which have been marked with method annotations. If the annotation does not match the allowed value the methods are not executed. This seemed to me to be a nice idea no one uses but who knows. It is here for convenience. | Firstly, these annotations are for method which have been marked with method annotations. If the annotation does not match the allowed value the methods are not executed. This seemed to me to be a nice idea no one uses but who knows. It is here for convenience. | ||
Line 292: | Line 339: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Authenticate with Database== | ==Authenticate with Database== | ||
*Create Database in Docker | *Create Database in Docker | ||
Line 423: | Line 408: | ||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Authenticate with LDAP== | ==Authenticate with LDAP== | ||
===Add Dependencies=== | ===Add Dependencies=== | ||
Line 473: | Line 433: | ||
spring.ldap.embedded.port=8389 | spring.ldap.embedded.port=8389 | ||
</syntaxhighlight> | </syntaxhighlight> | ||
===Add Initializer to App=== | |||
===Add Web Security Configurer Adapter=== | |||
<syntaxhighlight lang="bash"> | |||
@Configuration | |||
@EnableWebSecurity | |||
@EnableGlobalMethodSecurity ( | |||
prePostEnabled = true, | |||
securedEnabled = true, | |||
jsr250Enabled = true | |||
) | |||
public class Restapi11SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { | |||
@Override | |||
protected void configure(final AuthenticationManagerBuilder auth) throws Exception { | |||
auth.inMemoryAuthentication() | |||
.withUser("iwiseman").password(passwordEncoder().encode("pass")).roles("USER"); | |||
} | |||
@Bean | |||
public PasswordEncoder passwordEncoder() { | |||
return new BCryptPasswordEncoder(); | |||
} | |||
} | |||
</syntaxhighlight> | |||
===Amend WebSecurityConfigurerAdapter=== | ===Amend WebSecurityConfigurerAdapter=== | ||
The ConferenceUserDetailsContextMapper is a way to extend the user details and is explained in more detail below. | The ConferenceUserDetailsContextMapper is a way to extend the user details and is explained in more detail below. | ||
Line 626: | Line 612: | ||
.logoutSuccessUrl("/"); | .logoutSuccessUrl("/"); | ||
} | } | ||
</syntaxhighlight> | |||
=Rest APIs= | |||
==Basic== | |||
Spring provides filters which allows incoming requests for basic authentication.<br> | |||
[[File:Spring REST API Basic.png|400px]] | |||
Whilst this does work it has the following drawbacks | |||
*Use credentials are sent every call | |||
*Sensitive information is stored on the client | |||
*Having the credentials on the client may leave the server open to abuse via misuse | |||
==Bearer== | |||
A better approach is to use a bearer token where temporary authority is obtained and passed to the requests. <br> | |||
[[File:Sprint REST API Bearer.png|400px]] | |||
We will need the following dependencies | |||
<syntaxhighlight lang="java"> | |||
<dependency> | |||
<groupId>org.springframework.boot</groupId> | |||
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>org.springframework.security</groupId> | |||
<artifactId>spring-security-oauth2-jose</artifactId> | |||
</dependency> | |||
</syntaxhighlight> | |||
Define where the authorisation server is in application.yml | |||
<syntaxhighlight lang="yaml"> | |||
spring: | |||
security: | |||
oauth2: | |||
client: | |||
provider: | |||
one: | |||
issuer-uri: http://idp:9999/auth/realms/one | |||
</syntaxhighlight> | |||
==Direct Object Reference Vulnerability== | |||
I we call a REST API with a valid token there is nothing to stop the working. In Spring we are provided with the @PostAuthorize annotation. We need to <br><br> | |||
add the application annotation | |||
<syntaxhighlight lang="java"> | |||
@SpringBootApplication | |||
@EnableGlobalMethodSecurity(prePostEnabled = true) | |||
public class ResourceApplication { | |||
... | |||
</syntaxhighlight> | |||
Create a been to simplify the code | |||
<syntaxhighlight lang="java"> | |||
@Bean | |||
BiFunction<Optional<Resolution>, String, Boolean> owner() { | |||
return(resolution, userId -> resolution | |||
.filter(r -> r.getOwner().toString().equals(userId)) | |||
isPresent(); | |||
} | |||
</syntaxhighlight> | |||
In the controller we can now add the @PostAuthorize with the two parameters | |||
<syntaxhighlight lang="java"> | |||
@GetMapping("/resolution/{id}") | |||
@PostAuthorize("@owner.apply(returnObject, principle.claims['user_id']") | |||
public Optional<Resolution> read(@PathVariable("id") UUID id) { | |||
return this.resolutions.findById(id); | |||
} | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Latest revision as of 04:59, 3 April 2021
Introduction
Authentication
Spring Security Provides out of the box
- Basic Web From Authentication
- Ouath2 and OpenID Connect
- LDAP
- JWT JSon Web Tokens
Protection
Includes strategies for
- Session Fixation (Reusing of the Session ID)
- Clickjacking(UI redress attack)
- Cross Site Scripting
- Cross Site Request Forgery (CSRF)
Session Fixation (Reusing of the Session ID)
Session Fixation is an attack that permits an attacker to hijack a valid user session. The attack explores a limitation in the way the web application manages the session ID, more specifically the vulnerable web application. When authenticating a user, it doesn’t assign a new session ID, making it possible to use an existent session ID. The attack consists of obtaining a valid session ID (e.g. by connecting to the application), inducing a user to authenticate himself with that session ID, and then hijacking the user-validated session by the knowledge of the used session ID. The attacker has to provide a legitimate Web application session ID and try to make the victim’s browser use it.
Clickjacking(UI redress attack)
This is when an attacker uses multiple transparent or opaque layers to trick a user into clicking on a button or link on another page when they were intending to click on the top level page. Thus, the attacker is “hijacking” clicks meant for their page and routing them to another page, most likely owned by another application, domain, or both.
Cross Site Request Forgery (CSRF)
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker’s choosing. If the victim is a normal user, a successful CSRF attack can force the user to perform state changing requests like transferring funds, changing their email address, and so forth. If the victim is an administrative account, CSRF can compromise the entire web application.
Cross Site Scripting
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker’s choosing. If the victim is a normal user, a successful CSRF attack can force the user to perform state changing requests like transferring funds, changing their email address, and so forth. If the victim is an administrative account, CSRF can compromise the entire web application.
Spring Security Projects
The framework is broken own into different projects
- Spring Security Core
- Spring Security Config
- Spring Security Test
- Spring Security Web
- Spring Security Oauth
- Spring Security LDAP
Resources
Demos
https://github.com/wlesniak/spring-framework-securing-against-common-threats https://github.com/bh5k/spring-security-conference
Tomcat
Install Ubuntu 20.1
sudo useradd -m -U -d /opt/tomcat -s /bin/false tomcat
VERSION=9.0.35
wget https://www-eu.apache.org/dist/tomcat/tomcat-9/v${VERSION}/bin/apache-tomcat-${VERSION}.tar.gz -P /tmp
sudo tar -xf /tmp/apache-tomcat-${VERSION}.tar.gz -C /opt/tomcat/
sudo ln -s /opt/tomcat/apache-tomcat-${VERSION} /opt/tomcat/latest
sudo chown -R tomcat: /opt/tomcat
sudo sh -c 'chmod +x /opt/tomcat/latest/bin/*.sh'
sudo vi /etc/systemd/system/tomcat.service
Add the startup file
[Unit]
Description=Tomcat 9 servlet container
After=network.target
[Service]
Type=forking
User=tomcat
Group=tomcat
Environment="JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64"
Environment="JAVA_OPTS=-Djava.security.egd=file:///dev/urandom -Djava.awt.headless=true"
Environment="CATALINA_BASE=/opt/tomcat/latest"
Environment="CATALINA_HOME=/opt/tomcat/latest"
Environment="CATALINA_PID=/opt/tomcat/latest/temp/tomcat.pid"
Environment="CATALINA_OPTS=-Xms512M -Xmx1024M -server -XX:+UseParallelGC"
ExecStart=/opt/tomcat/latest/bin/startup.sh
ExecStop=/opt/tomcat/latest/bin/shutdown.sh
[Install]
WantedBy=multi-user.target
And update systemctl
sudo systemctl daemon-reload
sudo systemctl enable --now tomcat
sudo systemctl status tomcat
Web Management
# Add above </tomcat-users>
sudo vi /opt/tomcat/latest/conf/tomcat-users.xml
<role rolename="admin-gui"/>
<role rolename="manager-gui"/>
<user username="NotThis" password="NotThisEither" roles="admin-gui,manager-gui"/>
And to enable
# Manager App
sudo vi /opt/tomcat/latest/webapps/manager/META-INF/context.xml
# Host Manager App
sudo vi /opt/tomcat/latest/webapps/host-manager/META-INF/context.xml
Then add your host
<Context antiResourceLocking="false" privileged="true" >
<CookieProcessor className="org.apache.tomcat.util.http.Rfc6265CookieProcessor"
sameSiteCookies="strict" />
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1|192.168.50.1" />
<Manager sessionAttributeValueClassNameFilter="java\.lang\.(?:Boolean|Integer|Long|Number|String)|org\.apache\.catalina\.filters\.CsrfPreventionFilter\$LruCache(?:\$1)?|java\.util\.(?:Linked)?HashMap"/>
Deploy in VS Code
I installed the tomcat and maven extension. Loaded up a dummy project from https://github.com/branflake2267/debugging-java-webapp and followed the instructions.
- clone the project
- goto Maven->plugins->war and right click war:exploded
- install tomcat for java extension for VS CODE. I have tons of trouble with this. Do not put anything in the settings and make sure the root tomcat has 777 permissions
- add a tomcat server
- goto the target project directory and right click run on tomcat server
Deploy in IntelliJ
First of all I needed to specify the location of the JDK when building the project.
env |grep JAVA_HOME
Going to Maven->Package->Build resulted in "Failed to execute goal org.apache.maven.plugins:maven-surefile-plugin:2.22 I had to add the following
Open your project, build and go to the Maven tab on the right. Select->lifecycle->right click package->Select Modify Configuration->
Authentication
Architecture
Spring uses Filters for requests coming into the application like interceptors in Angular. The code we add will amend or create existing filter to change the behaviour of our application.
Package Dependencies
We can add security to a new project with
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
WebSecurityConfigurerAdapter
The WebSecurityConfigurerAdapter is a key class to configuring how spring behaves. I hope to go through and list the key points in this section.
Example Implementation
Here is an example of the entire configure method. We will go through and explain the parts as we go.
public class ConferenceSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private ConferenceUserDetailsContextMapper ctxMapper;
@Override
protected void configure(final HttpSecurity http) throws Exception {
http
.authorizeRequests()
//.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/anonymous*").anonymous()
.antMatchers("/login*").permitAll()
.antMatchers("/account*").permitAll()
.antMatchers("/password*").permitAll()
.antMatchers("/assets/css/**", "assets/js/**", "/images/**").permitAll()
.antMatchers("/index*").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/perform_login")
.failureUrl("/login?error=true")
.permitAll()
.defaultSuccessUrl("/", true)
.and()
.rememberMe()
.key("superSecretKey")
.tokenRepository(tokenRepository())
.and()
.logout()
.logoutSuccessUrl("/login?logout=true")
.logoutRequestMatcher(new AntPathRequestMatcher("/perform_logout", "GET"))
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll();
}
Method Security
Firstly, these annotations are for method which have been marked with method annotations. If the annotation does not match the allowed value the methods are not executed. This seemed to me to be a nice idea no one uses but who knows. It is here for convenience.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity (
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class ConferenceSecurityConfig extends WebSecurityConfigurerAdapter {
And an example of usage where @Secured only allows a user to run the function.
@Controller
public class RegistrationController {
@GetMapping("registration")
public String getRegistration(@ModelAttribute ("registration")Registration registration) {
return "registration";
}
@PostMapping("registration")
@Secured("ROLE_USER")
public String addRegistration(@Valid @ModelAttribute ("registration")
Registration registration,
BindingResult result,
Authentication auth) {
System.out.println("Auth: " + auth.getPrincipal());
if(result.hasErrors()) {
System.out.println("There were errors");
return "registration";
}
Ant Matchers
The antMatchers is keyword controls how endpoints can be accessed. The word ant comes from the apache project ant. We need to ensure, for instance, assets and images are allowed to be accessed.
protected void configure(final HttpSecurity http) throws Exception {
http
.authorizeRequests()
//.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/anonymous*").anonymous()
.antMatchers("/login*").permitAll()
.antMatchers("/account*").permitAll()
.antMatchers("/password*").permitAll()
.antMatchers("/assets/css/**", "assets/js/**", "/images/**").permitAll()
.antMatchers("/index*").permitAll()
...
Login
Here is the login logic. You will need to create the JSP pages to support this
...
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/perform_login")
.failureUrl("/login?error=true")
.permitAll()
.defaultSuccessUrl("/", true)
Logout
Here is the logout logic. You will need to create the JSP pages to support this. The Delete cookie is for the remember me feature. See below
.and()
.logout()
.logoutSuccessUrl("/login?logout=true")
.logoutRequestMatcher(new AntPathRequestMatcher("/perform_logout", "GET"))
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll();
Remember Me
To add Remember me you need to create a cookie and set a checkbox up with the remember-me name. Changing this name requires you to override it. I.E. this is what Spring expects
<form:form action="perform_login" method="post">
<form:errors path="*" cssClass="errorblock" element="div" />
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><label> Remember Me: <input type="checkbox" name="remember-me" /> </label></div>
<input type="submit" class="btn btn-lg btn-primary" role="button" value="Login"/>
<a href="password">Forgot password</a>
</form:form>
We also need to change WebSecurityConfigurerAdapter as ever
public class ConferenceSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(final HttpSecurity http) throws Exception {
http
.authorizeRequests()
...
.and()
.rememberMe()
.key("superSecretKey")
.tokenRepository(tokenRepository())
...
And we need to implement tokenRepository
@Bean
public PersistentTokenRepository tokenRepository () {
JdbcTokenRepositoryImpl token = new JdbcTokenRepositoryImpl();
token.setDataSource(dataSource);
return token;
}
Now Create a table to store the data
CREATE TABLE persistent_logins (
username VARCHAR(50) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL,
FOREIGN KEY (username) REFERENCES users(username)
);
MvcConfigurer
We can now direct the Spring Mvc Configurer to use the page with.
@Configuration
public class ConferenceConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(final ViewControllerRegistry registry) {
registry.addViewController("/login");
}
...
Authenticate with Database
- Create Database in Docker
- Create Schema
- Add Dependencies
- Amend Application Properties
Create Database in Docker
Had grief with this because I already run the database locally. I original changed the ports to be 3307 for both host and container but did not have joy. Ended up with
version: '3.8'
networks:
default:
services:
db:
image: mysql:5.7
container_name: conference_security
ports:
- 3307:3306
volumes:
- "./.data/db:/var/lib/mysql"
environment:
MYSQL_ROOT_PASSWORD: pass
MYSQL_DATABASE: TEST_DB
For there you can do the following
sudo docker-compose up -d
mysql -u root -p TEST_DB -h 127.0.0.1 -P 3307
Create Schema
CREATE TABLE users (
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
enabled TINYINT NOT NULL DEFAULT 1,
PRIMARY KEY (username)
);
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
FOREIGN KEY (username) REFERENCES users(username)
);
CREATE UNIQUE INDEX ix_auth_username on authorities (username, authority);
INSERT INTO users (username, password, enabled)
values (
'iwiseman',
'$2a$10$a07FaSKwo2xAwEj4UJYa0etu8sY5o9onG/0psQ2FxzjviueQUYnbm',
1
);
INSERT INTO authorities (username, authority)
values ('iwiseman', 'ROLE_USER');
Add Dependencies
We need to add a database for authentication. To do this we add the driver to the Pom.
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
Amend Application Properties
spring.datasource.url=jdbc:mysql://localhost:3307/conference_security
spring.datasource.username=root
spring.datasource.password=pass
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
Authenticate with LDAP
Add Dependencies
For an in memory LDAP we need the following dependencies.
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
</dependency>
Amend Application Properties
spring.ldap.embedded.ldif=classpath:test-server.ldif
spring.ldap.embedded.base-dn=dc=bibble,dc=co,dc=nz
spring.ldap.embedded.port=8389
Add Initializer to App
Add Web Security Configurer Adapter
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity (
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class Restapi11SecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("iwiseman").password(passwordEncoder().encode("pass")).roles("USER");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Amend WebSecurityConfigurerAdapter
The ConferenceUserDetailsContextMapper is a way to extend the user details and is explained in more detail below.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity (
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class ConferenceSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private ConferenceUserDetailsContextMapper ctxMapper;
...
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
// Swap out in memory for jdbc
// auth.inMemoryAuthentication()
//.withUser("iwiseman").password(passwordEncoder().encode("pass")).roles("USER");
// Swap out DataSource
// auth.jdbcAuthentication()
// .dataSource(dataSource)
// .passwordEncoder(passwordEncoder());
auth.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.contextSource()
.url("ldap://localhost:8389/dc=bibble,dc=co,dc=nz")
.and()
.passwordCompare()
.passwordEncoder(passwordEncoder())
.passwordAttribute("userPassword")
.and()
.userDetailsContextMapper(ctxMapper);
Customise the User
Sometime we want to extend the user to have more attributes maybe from different sources. In this example to are merging the DataSource attributes to the LDAP attributes. We can do this by
- Creating our Use Model
- Implementing the UserDetailsContextMapper to map User From (or to) Context
Creating our Use Model
public class ConferenceUserDetails extends org.springframework.security.core.userdetails.User {
private String nickname;
public ConferenceUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
}
Implementing the UserDetailsContextMapper
@Service
public class ConferenceUserDetailsContextMapper implements UserDetailsContextMapper {
@Autowired
private DataSource dataSource;
private static final String loadUserByUsernameQuery = "select username, password, " +
"enabled, nickname from users where username = ?";
@Override
public UserDetails mapUserFromContext(DirContextOperations dirContextOperations, String s, Collection<? extends GrantedAuthority> collection) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
final ConferenceUserDetails userDetails = new ConferenceUserDetails(
dirContextOperations.getStringAttribute("uid"),
"fake",
Collections.EMPTY_LIST);
jdbcTemplate.queryForObject(loadUserByUsernameQuery, new RowMapper<ConferenceUserDetails>() {
@Override
public ConferenceUserDetails mapRow(ResultSet resultSet, int i) throws SQLException {
userDetails.setNickname(resultSet.getString("nickname"));
return userDetails;
}
}, dirContextOperations.getStringAttribute("uid"));
return userDetails;
}
@Override
public void mapUserToContext(UserDetails userDetails, DirContextAdapter dirContextAdapter) {
}
}
Success Handling
We can define our own Success Handler by
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//do some logic here if you want something to be done whenever
//the user successfully logs in.
HttpSession session = httpServletRequest.getSession();
User authUser = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
session.setAttribute("username", authUser.getUsername());
session.setAttribute("authorities", authentication.getAuthorities());
//set our response to OK status
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
//since we have created our custom success handler, its up to us to where
//we will redirect the user after successfully login
httpServletResponse.sendRedirect("home");
}
}
From here we can add the class to the formLogin method.
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.authorizeRequests()
.mvcMatchers(HttpMethod.GET,"/","/index.html","/portfolio").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/api/login").successHandler(new CustomAuthenticationSuccessHandler())
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessUrl("/");
}
Rest APIs
Basic
Spring provides filters which allows incoming requests for basic authentication.
Whilst this does work it has the following drawbacks
- Use credentials are sent every call
- Sensitive information is stored on the client
- Having the credentials on the client may leave the server open to abuse via misuse
Bearer
A better approach is to use a bearer token where temporary authority is obtained and passed to the requests.
We will need the following dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
Define where the authorisation server is in application.yml
spring:
security:
oauth2:
client:
provider:
one:
issuer-uri: http://idp:9999/auth/realms/one
Direct Object Reference Vulnerability
I we call a REST API with a valid token there is nothing to stop the working. In Spring we are provided with the @PostAuthorize annotation. We need to
add the application annotation
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceApplication {
...
Create a been to simplify the code
@Bean
BiFunction<Optional<Resolution>, String, Boolean> owner() {
return(resolution, userId -> resolution
.filter(r -> r.getOwner().toString().equals(userId))
isPresent();
}
In the controller we can now add the @PostAuthorize with the two parameters
@GetMapping("/resolution/{id}")
@PostAuthorize("@owner.apply(returnObject, principle.claims['user_id']")
public Optional<Resolution> read(@PathVariable("id") UUID id) {
return this.resolutions.findById(id);
}
Registration Service
The basic functionality was to create a Model, View and Controller. In the example I watched they created a Repository to manage the account and password data access and a service to manage the service interface. There was no Spring Security related items during the demo so whilst it was interesting it was not related.
Repository Interfaces
Account Repository
public interface AccountRepository {
public Account create (Account account);
void saveToken(VerificationToken verificationToken);
VerificationToken findByToken(String token);
Account findByUsername(String username);
void createUserDetails(ConferenceUserDetails userDetails);
void createAuthorities(ConferenceUserDetails userDetails);
void delete(Account account);
void deleteToken(String token);
}
Password Repository
public interface PasswordRepository {
void saveToken(ResetToken resetToken);
ResetToken findByToken(String token);
void update(Password password, String username);
}
Service Interfaces
Account Service
public interface AccountService {
public Account create (Account account);
void createVerificationToken(Account account, String token);
void confirmAccount(String token);
}
Password Service
public interface PasswordService {
void createResetToken(Password password, String token);
boolean confirmResetToken(ResetToken token);
void update(Password password, String username);
}
Database Schema
To support this a schema was created.
CREATE TABLE accounts (
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
token VARCHAR(64) NOT NULL,
firstname VARCHAR(50) NOT NULL,
lastname VARCHAR(50) NOT NULL,
PRIMARY KEY (username)
);
Sending Email
We need to send emails when accounts are created. To do this we use the built in spring package.
Add Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
Amend Application Properties
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=test123@gmail.com
spring.mail.password=
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
Create An Event
Here is an example of the Publish and Event from spring. This is used to tell the controller to send the email
public class OnCreateAccountEvent extends ApplicationEvent {
private String appUrl;
private Account account;
public OnCreateAccountEvent(Account account, String appUrl) {
super(account);
this.account = account;
this.appUrl = appUrl;
}
public String getAppUrl() {
return appUrl;
}
public Account getAccount() {
return account;
}
}
Fire the Event in Our Controller
@Controller
public class AccountController {
@Autowired
private ApplicationEventPublisher eventPublisher;
...
eventPublisher.publishEvent(new OnCreateAccountEvent(account,"conference_war"));
...
Implement the Listener
@Component
public class AccountListener implements ApplicationListener<OnCreateAccountEvent> {
private String serverUrl = "http://localhost:8080/";
@Autowired
private JavaMailSender mailSender;
@Autowired
private AccountService accountService;
@Override
public void onApplicationEvent(OnCreateAccountEvent event) {
this.confirmCreateAccount(event);
}
private void confirmCreateAccount(OnCreateAccountEvent event) {
//get the account
//create verification token
Account account = event.getAccount();
String token = UUID.randomUUID().toString();
accountService.createVerificationToken(account, token);
//get email properties
String recipientAddress = account.getEmail();
String subject = "Account Confirmation";
String confirmationUrl = event.getAppUrl() + "/accountConfirm?token=" + token;
String message = "Please confirm:";
//send email
SimpleMailMessage email = new SimpleMailMessage();
email.setTo(recipientAddress);
email.setSubject(subject);
email.setText(message + "\r\n" + serverUrl + confirmationUrl);
mailSender.send(email);
}
}
Common Security Threats
Introduction
Spring Security provides default headers which can be customised
Caching
By default the headers turn off caching to avoid data being left behind in the browser. You can of course switch this on and it is recommended you do this on a page by page approach. To do this
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers().cacheControl().disable()
.mvcMatchers("/login").permitAll()
...
And in the Controller
@GetMapping("/users/{name}")
public ResponseEntity<UserDto> getUser(@PathVariable String name) {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS))
.body(new UserDto(name));
}
Default Headers
Spring Security has a variety of default headers to protect the user. Try and understand them before ever changing the defaults. These include
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY // ClickJacking
CSRF
This is implemented by default and generates a token like Microsoft Forgery tokens
Content Security Policy
This is turned off by default but when enable it can confine scripts to given domains for example. Well worth a look.
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers().contentSecurityPolicy("script-src: http://www.bibble....)
Http Firewall
Spring Security comes with a two types of firewall but of course you can override it to your needs
/**
* Allow url encoded slash http firewall.
*
* @return the http firewall
*/
@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
DefaultHttpFirewall firewall = new DefaultHttpFirewall();
firewall.setAllowUrlEncodedSlash(true);
return firewall;
}
HTTPS
Create a Keystore and Certificate
To create the JKS keystore
keytool -genkeypair -alias tomcat -keyalg RSA -keysize 2048 -keystore keystore.jks -validity 3650 -storepass password
To create the Certificate
keytool -genkeypair -alias tomcat -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore keystore.p12 -validity 3650 -storepass password
Where
- genkeypair: generates a key pair
- alias: the alias name for the item we are generating
- keyalg: the cryptographic algorithm to generate the key pair
- keysize: the size of the key. We have used 2048 bits, but 4096 would be a better choice for production
- storetype: the type of keystore
- keystore: the name of the keystore
- validity: validity number of days
- storepass: a password for the keystore
Verify the Keystore Content and Convert to PKCS12
keytool -list -v -keystore keystore.jks
keytool -list -v -storetype pkcs12 -keystore keystore.p12
keytool -importkeystore -srckeystore keystore.jks -destkeystore keystore.p12 -deststoretype pkcs12
Enable in Application (Application.yml)
server:
ssl:
key-store: classpath:keystore.p12
key-store-password: password
key-store-type: pkcs12
key-alias: tomcat
key-password: password
port: 8443
Configure Redirects
In the Spring Boot application we can now redirect with
@SpringBootApplication
public class WebApplication {
...
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
@Bean
public ServletWebServerFactory servletContainer() {
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(redirectConnector());
return tomcat;
}
private Connector redirectConnector() {
Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8080);
connector.setRedirectPort(8443);
return connector;
}
}
Spring Security As a Backend
You can use spring as backend to standardise you authentication. So we can use spring in place of authentication in React or Angular.
Managing Secrets
To management secrets like for the key-store we can use Jasypt. We can put the encoded value in the Application.yml and put the password in and environment variable. Was not impressed with what I learned but did recommend auditing properly i.e. logging recording access with who when and why as secret was accessed.
Exception Handling
You can manage various codes with in Spring and provide your own page. Below is an example of AccessDeniedHandler. Simply create a class and decide what you would like to have happen. E.g. Audit the problem
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private String errorPage;
public CustomAccessDeniedHandler() {
}
public CustomAccessDeniedHandler(String errorPage) {
this.errorPage = errorPage;
}
public String getErrorPage() {
return errorPage;
}
public void setErrorPage(String errorPage) {
this.errorPage = errorPage;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException {
//You can redirect to errorpage
response.sendRedirect(errorPage);
}
}
Make sure you add this to the WebSecurityConfigurerAdapter
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.accessDeniedPage("/access denise")
.accessDeniedHandler(new AccessDeniedHandler())
...