Spring Security

From bibbleWiki
Jump to navigation Jump to search

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. Spring Security Arc.png

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.
Spring REST API Basic.png 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.
Sprint REST API Bearer.png 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. Spring Security SPA.png

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())
 ...