Spring Security

Java EE security

  • Lacks depth for typical enterprise applications
  • Not portable between environments  (WAR/EAR)

Spring security

  • Authentication - establishing principal (user, device, system)
  • Authorizatino - can principal execute operation?

Authentication

  • HTTP BASIC authentication headers (an IETF RFC-based standard)
  • HTTP Digest authentication headers (an IETF RFC-based standard)
  • HTTP X.509 client certificate exchange (an IETF RFC-based standard)
  • LDAP (a very common approach to cross-platform authentication needs, especially in large environments)
  • Form-based authentication (for simple user interface needs)
  • OpenID authentication
  • ...
  • ...
  • ...

Authorisation

  • Web requests
  • Method invocation
  • Access to individual domain object instance

Packages

  • spring-security-core.jar
  • spring-security-remoting.jar
  • spring-security-web.jar
  • spring-security-config.jar
  • spring-security-ldap.jar
  • spring-security-acl.jar
  • spring-security-cas.jar
  • spring-security-test.jar

Configuration

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.inMemoryAuthentication()
				.withUser("user").password("password").roles("USER");
	}
}

Configuration

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.anyRequest().authenticated()
			.and()
		.formLogin()
			.and()
		.httpBasic();
    }
}
<http>
	<intercept-url pattern="/**" access="authenticated"/>
	<form-login />
	<http-basic />
</http>

Form login

protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.anyRequest().authenticated()
			.and()
		.formLogin()
			.loginPage("/login")
			.permitAll(); 

Authorisation

protected void configure(HttpSecurity http) throws Exception {
  http
    .authorizeRequests()
      .antMatchers("/resources/**", "/signup", "/about").permitAll()
      .antMatchers("/admin/**").hasRole("ADMIN")
      .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
      .anyRequest().authenticated()
      .and()
    // ...
    .formLogin();
}

Handling logouts

protected void configure(HttpSecurity http) throws Exception {
  http
    .logout()
      .logoutUrl("/my/logout")
      .logoutSuccessUrl("/my/index")
      .logoutSuccessHandler(logoutSuccessHandler)
      .invalidateHttpSession(true)
      .addLogoutHandler(logoutHandler)
      .deleteCookies(cookieNamesToClear)
      .and()
    ...
}

LogoutHandler and LogoutSuccessHandler

  • PersistentTokenBasedRememberMeServices
  • TokenBasedRememberMeServices
  • CookieClearingLogoutHandler
  • CsrfLogoutHandler
  • SecurityContextLogoutHandler
  • SimpleUrlLogoutSuccessHandler
  • HttpStatusReturningLogoutSuccessHandler

Authentication

  • In memory
  • JDBC
  • LDAP
  • Custom

In memory authentication

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
  auth
    .inMemoryAuthentication()
      .withUser("user").password("password").roles("USER").and()
      .withUser("admin").password("password").roles("USER", "ADMIN");
}

JDBC authentication

@Autowired
private DataSource dataSource;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
  auth
    .jdbcAuthentication()
      .dataSource(dataSource)
      .withDefaultSchema()
      .withUser("user").password("password").roles("USER").and()
      .withUser("admin").password("password").roles("USER", "ADMIN");
}

LDAP authentication

@Autowired
private DataSource dataSource;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
  auth
    .ldapAuthentication()
      .userDnPatterns("uid={0},ou=people")
      .groupSearchBase("ou=groups");
}
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password

dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password

dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
uniqueMember: uid=user,ou=people,dc=springframework,dc=org

dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org

Custom AuthenticationProvider

public interface AuthenticationProvider {
  Authentication authenticate(Authentication authentication)
      throws AuthenticationException;
  boolean supports(Class<?> authentication);
}
@Bean
public SpringAuthenticationProvider springAuthenticationProvider() {
  return new SpringAuthenticationProvider();
}

Custom UserDetailsService and PasswordEncoder

public interface UserDetailsService {
  UserDetails loadUserByUsername(String username)
    throws UsernameNotFoundException;
}
@Bean
public SpringDataUserDetailsService springDataUserDetailsService() {
  return new SpringDataUserDetailsService();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
  return new BCryptPasswordEncoder();
}

Method security (@Secured)

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}
public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

Method security (@PreAuthorize)

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}
public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

Method security and GlobalMethodSecurityConfiguration

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
	@Override
	protected MethodSecurityExpressionHandler createExpressionHandler() {
		// ... create and return custom MethodSecurityExpressionHandler ...
		return expressionHandler;
	}
}

XML Configuration

web.xml

<filter>
  <filter-name>springSecurityFilterChain</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
  <filter-name>springSecurityFilterChain</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

xmlns:security

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/security
		http://www.springframework.org/schema/security/spring-security.xsd">
	...
</beans>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/security
		http://www.springframework.org/schema/security/spring-security.xsd">
	...
</beans:beans>

Minimal <http> configuration

<http>
  <intercept-url pattern="/**" access="hasRole('USER')" />
  <form-login />
  <logout />
</http>

Authentication

<authentication-manager>
<authentication-provider>
	<user-service>
	<user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
	<user name="bob" password="bobspassword" authorities="ROLE_USER" />
	</user-service>
</authentication-provider>
</authentication-manager>

Authentication

<authentication-manager>
	<authentication-provider user-service-ref='myUserDetailsService'/>
</authentication-manager>
<authentication-manager>
<authentication-provider>
	<jdbc-user-service data-source-ref="securityDataSource"/>
</authentication-provider>
</authentication-manager>
<authentication-manager>
	<authentication-provider ref='myAuthenticationProvider'/>
</authentication-manager>

Password encoder

<beans:bean name="bcryptEncoder"
  class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

<authentication-manager>
<authentication-provider>
  <password-encoder ref="bcryptEncoder"/>
  <user-service>
  <user name="jimi" password="d7e6351eaa13189a5a3641bab846c8e8c69ba39f"
      authorities="ROLE_USER, ROLE_ADMIN" />
  <user name="bob" password="4e7421b1b8765d8f9406d87e7cc6aa784c4ab97f"
      authorities="ROLE_USER" />
  </user-service>
</authentication-provider>
</authentication-manager>

Form login

<http>
  <intercept-url pattern="/login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
  <intercept-url pattern="/**" access="ROLE_USER" />
  <form-login login-page='/login.jsp'/>
</http>
<http pattern="/css/**" security="none"/>
<http pattern="/login.jsp*" security="none"/>

<http use-expressions="false">
  <intercept-url pattern="/**" access="ROLE_USER" />
  <form-login login-page='/login.jsp'/>
</http>
<http pattern="/login.htm*" security="none"/>
<http use-expressions="false">
  <intercept-url pattern='/**' access='ROLE_USER' />
  <form-login login-page='/login.htm' default-target-url='/home.htm'
		always-use-default-target='true' />
</http>

Basic authentication

<http use-expressions="false">
  <intercept-url pattern="/**" access="ROLE_USER" />
  <http-basic />
</http>

Filters

  • http://docs.spring.io/spring-security/site/docs/4.1.2.BUILD-SNAPSHOT/reference/html/ns-config.html#ns-custom-filters
  • We can create our own filters
<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myFilter" />
</http>

<beans:bean id="myFilter" class="com.mycompany.MySpecialAuthenticationFilter"/>

<global-method-security>

<global-method-security secured-annotations="enabled" />
public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}
<global-method-security pre-post-annotations="enabled" />
public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

Security pointcuts

<global-method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))"
	access="ROLE_USER"/>
</global-method-security>

Authentication Internals

SecurityContextHolder

  • Stores SecurityContext
  • Default store method is ThreadLocal but can be changed
  • SecurityContextHolder.getContext()
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
  String username = ((UserDetails)principal).getUsername();
} else {
  String username = principal.toString();
}

UserDetails

  • Adapter between Spring Security and databases
  • Provided by UserDetailsService
  • Multiple builtin implementations: InMemoryDaoImpl, JdbcDaoImpl
public interface UserDetailsService {
  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
public interface UserDetails extends Serializable {
  Collection<? extends GrantedAuthority> getAuthorities();
  String getPassword();
  String getUsername();
  boolean isAccountNonExpired();
  boolean isAccountNonLocked();
  boolean isCredentialsNonExpired();
  boolean isEnabled();
}

GrantedAuthority

  • Usually roles (ROLE_ADMINISTRATOR, ROLE_HR_SUPERVISOR)
Collection<? extends GrantedAuthority> authorities =
  SecurityContextHolder.getContext().getAuthentication().getAuthorities();
public interface GrantedAuthority extends Serializable {
  String getAuthority();
}

Authentication

  • Container Managed Authentication is not recommended
  1. Building UsernamePasswordAuthenticationToken (not fully populated of Authentication)
  2. Token is passed to AuthenticationManager
  3. On successful validation, AuthenticationManager returns fully pupulated Authentication
  4. SecurityContext is established and Authentication set

Authentication

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

public class AuthenticationExample {
  private static AuthenticationManager am = new SampleAuthenticationManager();

  public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

    while(true) {
      System.out.println("Please enter your username:");
      String name = in.readLine();
      System.out.println("Please enter your password:");
      String password = in.readLine();
      try {
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        Authentication result = am.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
      } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
      }
    }
    System.out.println("Successfully authenticated. Security context contains: " +
      SecurityContextHolder.getContext().getAuthentication());
  }
}

class SampleAuthenticationManager implements AuthenticationManager {
  static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

  static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
  }

  public Authentication authenticate(Authentication auth) throws AuthenticationException {
    if (auth.getName().equals(auth.getCredentials())) {
      return new UsernamePasswordAuthenticationToken(auth.getName(),
        auth.getCredentials(), AUTHORITIES);
    }
    throw new BadCredentialsException("Bad Credentials");
  }
}

AuthenticationManager

  • Default implemenation is ProviderManager
  • Delegates to AuthenticationProviders
  • AuthenticationProvider returns Authentication, null if not supported or throws an Exception

DaoAuthenticationProvider

<bean id="daoAuthenticationProvider"
    class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
  <property name="userDetailsService" ref="inMemoryDaoImpl"/>
  <property name="passwordEncoder" ref="passwordEncoder"/>
</bean>
public interface UserDetailsService {
  UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
public interface PasswordEncoder {
  String encode(CharSequence rawPassword);
  boolean matches(CharSequence rawPassword, String encodedPassword);
}

UserDetailsService

  • InMemoryUserDetailsManager
  • JdbcDaoImpl
    • Uses JDBC instead of ORM

PasswordEncoder

  • StandardPasswordEncoder (not recommended)
  • NoOpPasswordEncoder (for testing)
  • BCryptPasswordEncoder (best option!)

Authorization (Access Control) Internals

Internals

AccessDecisionManager

public interface AccessDecisionManager {
  void decide(Authentication authentication, Object object,
      Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
      InsufficientAuthenticationException;
  boolean supports(ConfigAttribute attribute);
  boolean supports(Class<?> clazz);
}

Secure Object

  • Object with security appled on it (method invocation, http requests, ...)
  • AbstractSecurityInterceptor for every secure object
    • MethodSecurityInterceptor
    • FilterSecurityInterceptor
    • AspectJMethodSecurityInterceptor

AbstractSecurityInterceptor

  1. Looks up the configuration attributes for request
  2. Invokes AccessDecisionManager with Authentication object
  3. Changes Authentication if necessary
  4. Proceeds secured object invocation on ganted permission
  5. Calls the AfterInvocationManager (if configured)

Testing

Given the service...

@service
public class HelloMessageService implements MessageService {

  @PreAuthorize("authenticated")
  public String getMessage() {
    Authentication authentication = SecurityContextHolder.getContext()
                              .getAuthentication();
    return "Hello " + authentication;
  }
}

Not authenticated

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
  messageService.getMessage();
}

@WithMockUser

@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
  String message = messageService.getMessage();
}
@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
  String message = messageService.getMessage();
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
  ...
}

@WithAnonymousUser

@RunWith(SpringJUnit4ClassRunner.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {

  @Test
  public void withMockUser1() {
  }

  @Test
  public void withMockUser2() {
  }

  @Test
  @WithAnonymousUser
  public void anonymous() throws Exception {
    // override default to run as anonymous user
  }
}

@WithUserDetails

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
  String message = messageService.getMessage();
  ...
}
@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
  String message = messageService.getMessage();
  ...
}

Meta annotations

@WithMockUser(username="admin",roles={"USER","ADMIN"})
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }

Spring Security

By Artur Owczarek

Spring Security

  • 610