Spring Security -- 5) Filter Chain, Custom filter and Authentication
Let’s look at the Filter Chain, more specifically AuthenticationFilter in the Spring Security. And also I am going to implement custom filter. …
In this post, let’s implement two steps authentication mechanism. This will be similar to JWT authentication but instead of JWT I will use my implementation. After legitimate user makes the login request, my response will include the header value called Authorization and user can be able to access to restricted endpoints using the Authorization header’s value.
You may see the previous post which includes simple project to understand filter, provider and authentication.
By the way, if you like the my blog contents, you can support me via patreon :)
If you only need to see the code, here is the github link
This project will be kind of a JWT token authentication. First, User will be authenticated against the database via username and password. After database authentication, I will generate one-time-password(otp) for that user which is actually random code. After that user will send request which also includes this new random otp. Then I will check the otp value, if the otp value is correct, then I will generate Authorization header’s value and save it to my database (in this simple project database will be the HashSet
, don’t do this in real case scenario(s)) and my response will include Authorization: {{Authorization header’s value }} header which is the random token to access the restricted endpoint(s).
For other requests than the /login
, I will get the Authorization header’s value and check the whether the value is correct or not. If it is not correct, authentication will fail.
This might not be considered as secure application. There could be situation where access token can be stolen!! For instance, otp value in the project has no expiration time, but in real case it should be…
After starting the project run the following command (I assume that there is user with test_user and password=1234 in your users
table )
If you try the connect the restricted endpoint without authorization, the you will get an exception:
$ curl -X GET http://localhost:8080/hello
// log in the console:
org.springframework.security.authentication.BadCredentialsException: Authorization value is not correct
at com.mehmetozanguven.springsecuritymultipleproviders.service.providers.TokenAuthProvider.authenticate(TokenAuthProvider.java:27) ~[classes/:na]
Before getting the authorization token, you should create an record into the otp table:
$ curl -H "username:test_user" -H "password:1234" -X GET http://localhost:8080/login
Right now look at the otp table, you should have one record:
testdatabase=# select * from otp;
id | username | otp
----+-----------+------------
1 | test_user | RXyObQYNDr
Right now, run the following curl command:
$ curl -H "username:test_user" -H "otp:RXyObQYNDr" -X GET http://localhost:8080/login -v
Note: Unnecessary use of -X or --request, GET is already inferred.
* Trying ::1:8080...
* Connected to localhost (::1) port 8080 (#0)
> GET /login HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.69.1
> Accept: */*
> username:test_user
> otp:RXyObQYNDr
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
-------
< Authorization: 7c60f6b3-4047-4fc6-8a37-267661c574f4
-------
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Length: 0
< Date: Mon, 04 Jan 2021 18:45:49 GMT
<
* Connection #0 to host localhost left intact
Authorization: 7c60f6b3-4047-4fc6-8a37-267661c574f4 is the token to access the restricted endpoint
Now access the restricted endpoint via Authorization value:
$ curl -H "Authorization:7c60f6b3-4047-4fc6-8a37-267661c574f4" -X GET http://localhost:8080/hello
hello
If you try to access with the wrong value:
$ curl -H "Authorization:test_value" -X GET http://localhost:8080/hello
// log in the console:
org.springframework.security.authentication.BadCredentialsException: Authorization value is not correct
at com.mehmetozanguven.springsecuritymultipleproviders.service.providers.TokenAuthProvider.authenticate(TokenAuthProvider.java:27) ~[classes/:na]
In this simple project, you will need an JPA and database connection setup (I am going to use Postgresql):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Do not forget to add connection setup to database, here is my configuration:
# Spring DATASOURCE for local postgresql
spring.datasource.url=jdbc:postgresql://localhost:5432/testdatabase
spring.datasource.username=postgres
spring.datasource.password=1234
# For localdb, it would be good to re-create all-tables
spring.jpa.hibernate.ddl-auto=update
And please setup empty table called users
which has 3 columns:
[mehmetozanguven@localhost ~]$ sudo -iu postgres
[postgres@localhost ~]$ psql
psql (12.5)
Type "help" for help.
postgres=# \c testdatabase
You are now connected to database "testdatabase" as user "postgres".
testdatabase=# drop table users ; // drop the table if you have previously...
DROP TABLE
testdatabase=# CREATE TABLE IF NOT EXISTS users (id serial PRIMARY KEY, username VARCHAR(50), password VARCHAR(50));
CREATE TABLE
You should also setup another table called otp
:
testdatabase=# CREATE TABLE IF NOT EXISTS otp (id serial PRIMARY KEY, username VARCHAR(50), otp VARCHAR(50));
CREATE TABLE
Do not forget to add test user:
testdatabase=# INSERT INTO users (username, password) VALUES ('test_user', '1234');
INSERT 0 1
testdatabase=# select * from users;
id | username | password
----+-----------+----------
1 | test_user | 1234
(1 row)
I will use NoopPasswordEncoder
for this simple project.
Controller:
package com.mehmetozanguven.springsecuritymultipleproviders.controllers;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
Entities:
package com.mehmetozanguven.springsecuritymultipleproviders.entities;
import javax.persistence.*;
@Entity
@Table(name = "users")
public class UserDTO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
}
package com.mehmetozanguven.springsecuritymultipleproviders.entities;
import javax.persistence.*;
@Entity
@Table(name = "otp")
public class OtpDTO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "username")
private String username;
@Column(name = "otp")
private String otp;
}
Repositories:
package com.mehmetozanguven.springsecuritymultipleproviders.repositories;
import com.mehmetozanguven.springsecuritymultipleproviders.entities.UserDTO;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<UserDTO, Long> {
Optional<UserDTO> findUserDTOByUsername(String username);
}
package com.mehmetozanguven.springsecuritymultipleproviders.repositories;
import com.mehmetozanguven.springsecuritymultipleproviders.entities.OtpDTO;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OtpRepository extends JpaRepository<OtpDTO, Long> {
}
Model to convert UserDTO
to UserDetails
object:
public class SecureUser implements UserDetails {
private final UserDTO userDTO;
public SecureUser(UserDTO userDTO) {
this.userDTO = userDTO;
}
/**
* Because I will not look at the authority part for now
* I am just creating dummy authority for the users
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> "read");
}
@Override
public String getPassword() {
return userDTO.getPassword();
}
@Override
public String getUsername() {
return userDTO.getUsername();
}
// ...
}
UserDetailsService:
package com.mehmetozanguven.springsecuritymultipleproviders.service;
public class PostgresqlUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
public PostgresqlUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDTO userInDb = userRepository.findUserDTOByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found in the db"));
return new SecureUser(userInDb);
}
}
Configuration:
@Configuration
public class ProjectBeanConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public PostgresqlUserDetailsService userDetailsService() {
return new PostgresqlUserDetailsService(userRepository);
}
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
}
I do not want to convert ServletRequest
to the HttpServletRequest
instead I will directly work on the HttpServletRequest
extending OncePerRequestFilter
And also I am going to override method OncePerRequestFilter#shouldNotFilter(HttpServletRequest request)
to determine for which path(s) this filter will be called.
This filter will be called when path is /login
public class UsernamePasswordAuthFilter extends OncePerRequestFilter {
// but I didn't defined any manager YET !!!
// let's define the one inside the configuration
private AuthenticationManager authenticationManager;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
String username = httpServletRequest.getHeader("username");
String password = httpServletRequest.getHeader("password");
String otp = httpServletRequest.getHeader("otp");
if (otp == null){
// authenticate via username and password and generate one time password
}else{
// authenticate via one time password and
// generate Authorization header with random value
// finally save the header's value elsewhere!!
}
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
if (isPathLogin(request)){
// if path is login then return false, which means: call the doFilterInternal
return false;
}else{
// do not call doFilterInternal
return true;
}
}
private boolean isPathLogin(HttpServletRequest request){
return request.getServletPath().equals("/login");
}
}
Before diving into authentication instances, first let’s define one manager in the configuration class:
@Configuration
public class ProjectBeanConfiguration extends WebSecurityConfigurerAdapter {
// ...
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
As you recall from the previous post , there could be Authentication
instance and AuthenticationManager
will call the appropriate AuthenticationProvider
based on the type of the Authentication instance(s). Thus, I should create Authentication classes for each authentication logics.
For simplicity, I will extend the UsernamePasswordAuthenticationToken
. Here is the Authentication class for otp authentication:
package com.mehmetozanguven.springsecuritymultipleproviders.service.authentications;
public class OtpAuthentication extends UsernamePasswordAuthenticationToken {
// this constructor creates a Authentication instance which is not fully authenticated
public OtpAuthentication(Object principal, Object credentials) {
super(principal, credentials);
}
// this constructor creates a Authentication instance which is fully authenticated
public OtpAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
For Username:password authentication, I will use UsernamePasswordAuthentication
package com.mehmetozanguven.springsecuritymultipleproviders.service.authentications;
public class UsernamePasswordAuthentication extends UsernamePasswordAuthenticationToken {
// this constructor creates a Authentication instance which is not fully authenticated
public UsernamePasswordAuthentication(Object principal, Object credentials) {
super(principal, credentials);
}
// this constructor creates a Authentication instance which is fully authenticated
public UsernamePasswordAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
After my Authentication instances are ready, Filter is ready to pass the instances to the AuthenticationManager
After all Filter will get the result from the Manager.
public class UsernamePasswordAuthFilter extends OncePerRequestFilter {
private AuthenticationManager authenticationManager;
private OtpRepository otpRepository;
private AuthorizationTokenHolder authorizationTokenHolder;
public UsernamePasswordAuthFilter(AuthenticationManager authenticationManager,
OtpRepository otpRepository,
AuthorizationTokenHolder authorizationTokenHolder) {
this.authenticationManager = authenticationManager;
this.otpRepository = otpRepository;
this.authorizationTokenHolder = authorizationTokenHolder;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
String username = httpServletRequest.getHeader("username");
String password = httpServletRequest.getHeader("password");
String otp = httpServletRequest.getHeader("otp");
if (otp == null){
// * if there is no one-time-password, then generate an OTP
// 1) first make sure that, legitimate user is connecting to the your system
UsernamePasswordAuthentication usernamePasswordAuthentication =
new UsernamePasswordAuthentication(username, password);
// 2) authenticationManager will find the correct provider for that authentication
Authentication resultForUsernamePassword = authenticationManager.authenticate(usernamePasswordAuthentication);
// 3) Because authenticationManager.authenticate will return fully authenticated instance
// ( otherwise it must throw an error), You can generate otp for authenticatied user
// 4) Generate new otp
String otpCode = RandomStringUtils.randomAlphabetic(10);
// 5) save this new one-time-password for that user.
OtpDTO otpDTO = new OtpDTO();
otpDTO.setUsername(username);
otpDTO.setOtp(otpCode);
otpRepository.save(otpDTO);
}else{
// if there is a one-time-password, authenticate user via one-time-password(otp)
OtpAuthentication otpAuthentication = new OtpAuthentication(username, otp);
// authenticationManager will find the correct provider for that authentication
Authentication resultForOtp = authenticationManager.authenticate(otpAuthentication);
// after getting fully authenticated instance, generate authorization header' value
String authValue = UUID.randomUUID().toString();
// save the authorization value for checking future request(s)
authorizationTokenHolder.add(authValue);
// add new header to the response
httpServletResponse.setHeader("Authorization", authValue);
}
}
// ...
}
What I am missing is that, there should be provider(s) for these authentication processes. Let’s define the providers
Let’s recap what the AuthenticationProvider does:
AuthenticationProvider
contains two methods: authenticate()
contains the authentication logic and supports()
contains the logic which this authentication provider should be applied or not.For the authenticate()
method, there are three options:
Right now, I need two AuthenticationProvider for each Authentication processes (OtpAuthentication && UsernamePasswordAuthentication
)
package com.mehmetozanguven.springsecuritymultipleproviders.service.providers;
public class UsernamePassswordAuthProvider implements AuthenticationProvider {
private PostgresqlUserDetailsService postgresqlUserDetailsService;
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String) authentication.getCredentials();
// loadByUsername will throw an exception if user is not found
UserDetails userDetails = postgresqlUserDetailsService.loadUserByUsername(username);
// after finding the user's details, check with the passwordEncoder
if (passwordEncoder.matches(password, userDetails.getPassword())){
// if everything is correct, create a fully authenticated object
// for this simple project, authorities is hard-coded, do not care about
return new UsernamePasswordAuthenticationToken(username, password, List.of(() -> "read"));
}
throw new BadCredentialsException("BadCredentialException");
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthentication.class.equals(authentication);
}
}
package com.mehmetozanguven.springsecuritymultipleproviders.service.providers;
public class OtpAuthProvider implements AuthenticationProvider {
private OtpRepository otpRepository;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String otp = (String) authentication.getCredentials();
OtpDTO otpDTO = otpRepository.findOtpDtoByUsername(username)
.orElseThrow(() -> new BadCredentialsException("Bad Otp Credentials"));
return new OtpAuthentication(username, otp, List.of(() -> "read"));
}
@Override
public boolean supports(Class<?> authentication) {
return OtpAuthentication.class.equals(authentication);
}
}
The left is to add these providers to the configuration
@Configuration
public class ProjectBeanConfiguration extends WebSecurityConfigurerAdapter {
public UsernamePassswordAuthProvider usernamePassswordAuthProvider() {
return new UsernamePassswordAuthProvider(...);
}
public OtpAuthProvider otpAuthProvider(){
return new OtpAuthProvider(...);
}
// ...
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(usernamePassswordAuthProvider())
.authenticationProvider(otpAuthProvider());
}
}
@Configuration
public class ProjectBeanConfiguration extends WebSecurityConfigurerAdapter {
public UsernamePasswordAuthFilter usernamePasswordAuthFilter() throws Exception {
return new UsernamePasswordAuthFilter(...);
}
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAt(usernamePasswordAuthFilter(), BasicAuthenticationFilter.class);
}
}
I can create one-time-password, and using this otp I can also generate Authorization header. It is time to authenticate (allow users to access restricted enpoints) using this Authorization header.
The second filter will look at the Authorization header in the request and it will allow the request to access endpoint or not.
Because UsernamePasswordAuthFilter
was responsible to process the authentication for only /login
endpoint, all other endpoints must be filtered.
package com.mehmetozanguven.springsecuritymultipleproviders.service.filters;
public class TokenAuthFilter extends OncePerRequestFilter {
private AuthenticationManager authenticationManager;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorization = request.getHeader("Authorization");
TokenAuthentication tokenAuthentication = new TokenAuthentication(null, authorization);
Authentication fullyAuthentication = authenticationManager.authenticate(tokenAuthentication);
SecurityContextHolder.getContext().setAuthentication(fullyAuthentication);
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
if (isPathLogin(request)){
// if path is login then return true,
// which means: do not run the TokenAuthFilter#doFilterInternal,
// this authentication must be done by UsernamePasswordAuthFilter
return true;
}else{
// for other endpoints run this authentication
return false;
}
}
private boolean isPathLogin(HttpServletRequest request){
return request.getServletPath().equals("/login");
}
}
As I have done previously, I should create a provider and authenticate instance for that filter. Provider will include the authentication login.
Right now, I will have TokenAuthentication
which extends UsernamePasswordAuthenticationToken
package com.mehmetozanguven.springsecuritymultipleproviders.service.authentications;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class TokenAuthentication extends UsernamePasswordAuthenticationToken {
// this constructor creates a Authentication instance which is not fully authenticated
public TokenAuthentication(Object principal, Object credentials) {
super(principal, credentials);
}
// this constructor creates a Authentication instance which is fully authenticated
public TokenAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
Now AuthenticationManager will call the correct authentication provider for that second filter. Let’s implement the provider
public class TokenAuthProvider implements AuthenticationProvider {
private AuthorizationTokenHolder authorizationTokenHolder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String authorizationToken = (String) authentication.getCredentials();
boolean isCorrectToken = authorizationTokenHolder.contains(authorizationToken);
if (isCorrectToken){
return new TokenAuthentication(null, authorizationToken, List.of(() -> "read"));
}else {
throw new BadCredentialsException("Authorization value is not correct");
}
}
@Override
public boolean supports(Class<?> authentication) {
return TokenAuthentication.class.equals(authentication);
}
}
The last thing is to add these new filters and providers to the configuration:
@Configuration
public class ProjectBeanConfiguration extends WebSecurityConfigurerAdapter {
// ...
// second filter and provider
public TokenAuthFilter tokenAuthFilter() {
// not using Autowired to avoid circular dependency issue
return new TokenAuthFilter();
}
public TokenAuthProvider tokenAuthProvider() {
return new TokenAuthProvider(...);
}
// ...
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.
...
.authenticationProvider(tokenAuthProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAt(usernamePasswordAuthFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(tokenAuthFilter(), BasicAuthenticationFilter.class);
}
}
I will continue with the next one …
Let’s look at the Filter Chain, more specifically AuthenticationFilter in the Spring Security. And also I am going to implement custom filter. …
In this post, I am going to answer to this question “what is the Authentication Provider” and I am going to implement a project includes …