Secure Spring Boot RESTful Service using Auth0 JWT
1. Restful Spring Boot & JWT
No ADS
It is assumed that you have a RESTful API written on Spring Boot, Clients (other applications) can call your RESTful API and receive a result.
However, not all RESTful APIs can be public, because of their sensitivity, therefore, you needs to secure them. There are some techniques for you to secure your RESTful API:
- Secure RESTful API with Basic Authentication.
- Secure RESTful API withJWT (JSON Web Token).
There are a few other techniques not listed by me above. But, basically, the Client wants to call a protected RESTful API, it needs to send a request attached with authentication information (possibly username/password).
See more:
- Tìm hiểu về JWT (JSON Web Token)
The Client's calling a public REST API is simple. It is like the following illustration:
In case, the REST API is secured by the Basic Authentication, the Client has to encrypt the "username:password" string with the Base64 algorithm to obtain a byte array, attach this array on Request Headers in each REST API call.
For the REST API secured with Auth0, calling will be a bit more complicated.
- Step 1: You have to send a login request containing username/password, and will receive a response which is a "Authorization String" attached on the Response Header.
- Step 2: After "Authorization String" is available, attache it on the Request Header to call the REST API.
2. Create a Spring Boot project
No ADS
On the Eclipse, create a Spring Boot project
Declare Auth0 libraries to be used in this project.
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/auth0 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0</artifactId>
<version>1.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/auth0-spring-security-api -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0-spring-security-api</artifactId>
<version>1.0.0</version>
</dependency>
The full content of pom.xml file:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>SpringBootJWT</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>SpringBootJWT</name>
<description>Spring Boot + Rest + JWT</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/auth0 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0</artifactId>
<version>1.5.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/auth0-spring-security-api -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0-spring-security-api</artifactId>
<version>1.0.0</version>
</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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
SpringBootJwtApplication.java
package org.o7planning.sbjwt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootJwtApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootJwtApplication.class, args);
}
}
3. Model, DAO & REST API
No ADS
Employee.java
package org.o7planning.sbjwt.model;
public class Employee {
private String empNo;
private String empName;
private String position;
public Employee() {
}
public Employee(String empNo, String empName, String position) {
this.empNo = empNo;
this.empName = empName;
this.position = position;
}
public String getEmpNo() {
return empNo;
}
public void setEmpNo(String empNo) {
this.empNo = empNo;
}
public String getEmpName() {
return empName;
}
public void setEmpName(String empName) {
this.empName = empName;
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
}
EmployeeDAO.java
package org.o7planning.sbjwt.dao;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.o7planning.sbjwt.model.Employee;
import org.springframework.stereotype.Repository;
@Repository
public class EmployeeDAO {
private static final Map<String, Employee> empMap = new HashMap<String, Employee>();
static {
initEmps();
}
private static void initEmps() {
Employee emp1 = new Employee("E01", "Smith", "Clerk");
Employee emp2 = new Employee("E02", "Allen", "Salesman");
Employee emp3 = new Employee("E03", "Jones", "Manager");
empMap.put(emp1.getEmpNo(), emp1);
empMap.put(emp2.getEmpNo(), emp2);
empMap.put(emp3.getEmpNo(), emp3);
}
public Employee getEmployee(String empNo) {
return empMap.get(empNo);
}
public Employee addEmployee(Employee emp) {
empMap.put(emp.getEmpNo(), emp);
return emp;
}
public Employee updateEmployee(Employee emp) {
empMap.put(emp.getEmpNo(), emp);
return emp;
}
public void deleteEmployee(String empNo) {
empMap.remove(empNo);
}
public List<Employee> getAllEmployees() {
Collection<Employee> c = empMap.values();
List<Employee> list = new ArrayList<Employee>();
list.addAll(c);
return list;
}
}
MainRESTController.java
package org.o7planning.sbjwt.controller;
import java.util.List;
import org.o7planning.sbjwt.dao.EmployeeDAO;
import org.o7planning.sbjwt.model.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MainRESTController {
@Autowired
private EmployeeDAO employeeDAO;
@RequestMapping("/")
@ResponseBody
public String welcome() {
return "Welcome to Spring Boot + REST + JWT Example.";
}
@RequestMapping("/test")
@ResponseBody
public String test() {
return "{greeting: 'Hello'}";
}
// URL:
// http://localhost:8080/employees
@RequestMapping(value = "/employees", //
method = RequestMethod.GET, //
produces = { MediaType.APPLICATION_JSON_VALUE, //
MediaType.APPLICATION_XML_VALUE })
@ResponseBody
public List<Employee> getEmployees() {
List<Employee> list = employeeDAO.getAllEmployees();
return list;
}
// URL:
// http://localhost:8080/employee/{empNo}
@RequestMapping(value = "/employee/{empNo}", //
method = RequestMethod.GET, //
produces = { MediaType.APPLICATION_JSON_VALUE, //
MediaType.APPLICATION_XML_VALUE })
@ResponseBody
public Employee getEmployee(@PathVariable("empNo") String empNo) {
return employeeDAO.getEmployee(empNo);
}
// URL:
// http://localhost:8080/employee
@RequestMapping(value = "/employee", //
method = RequestMethod.POST, //
produces = { MediaType.APPLICATION_JSON_VALUE, //
MediaType.APPLICATION_XML_VALUE })
@ResponseBody
public Employee addEmployee(@RequestBody Employee emp) {
System.out.println("(Service Side) Creating employee: " + emp.getEmpNo());
return employeeDAO.addEmployee(emp);
}
// URL:
// http://localhost:8080/employee
@RequestMapping(value = "/employee", //
method = RequestMethod.PUT, //
produces = { MediaType.APPLICATION_JSON_VALUE, //
MediaType.APPLICATION_XML_VALUE })
@ResponseBody
public Employee updateEmployee(@RequestBody Employee emp) {
System.out.println("(Service Side) Editing employee: " + emp.getEmpNo());
return employeeDAO.updateEmployee(emp);
}
// URL:
// http://localhost:8080/employee/{empNo}
@RequestMapping(value = "/employee/{empNo}", //
method = RequestMethod.DELETE, //
produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
@ResponseBody
public void deleteEmployee(@PathVariable("empNo") String empNo) {
System.out.println("(Service Side) Deleting employee: " + empNo);
employeeDAO.deleteEmployee(empNo);
}
}
4. Security & Login Filter
No ADS
This application has login function. The Client can send a login request withPOST method. Therefore, it is not necessary to create a login page. Instead, we have a Filter, when a request with a path/login, it will be processed by this filter.
The requests want to go to theController, they have to pass the Filters:
In this lesson, for simplicity, I will create 2 Users in the memory, the Client can log in with one of the two following users:
- tom/123
- jerry/123
WebSecurityConfig.java
package org.o7planning.sbjwt.config;
import org.o7planning.sbjwt.filter.JWTAuthenticationFilter;
import org.o7planning.sbjwt.filter.JWTLoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
// No need authentication.
.antMatchers("/").permitAll() //
.antMatchers(HttpMethod.POST, "/login").permitAll() //
.antMatchers(HttpMethod.GET, "/login").permitAll() // For Test on Browser
// Need authentication.
.anyRequest().authenticated()
//
.and()
//
// Add Filter 1 - JWTLoginFilter
//
.addFilterBefore(new JWTLoginFilter("/login", authenticationManager()),
UsernamePasswordAuthenticationFilter.class)
//
// Add Filter 2 - JWTAuthenticationFilter
//
.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
String password = "123";
String encrytedPassword = this.passwordEncoder().encode(password);
System.out.println("Encoded password of 123=" + encrytedPassword);
InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> //
mngConfig = auth.inMemoryAuthentication();
// Defines 2 users, stored in memory.
// ** Spring BOOT >= 2.x (Spring Security 5.x)
// Spring auto add ROLE_
UserDetails u1 = User.withUsername("tom").password(encrytedPassword).roles("USER").build();
UserDetails u2 = User.withUsername("jerry").password(encrytedPassword).roles("USER").build();
mngConfig.withUser(u1);
mngConfig.withUser(u2);
// If Spring BOOT < 2.x (Spring Security 4.x)):
// Spring auto add ROLE_
// mngConfig.withUser("tom").password("123").roles("USER");
// mngConfig.withUser("jerry").password("123").roles("USER");
}
}
When a request with path/login is sent to the Server, it will is processed by JWTLoginFilter. This class will check username/password, if they are valid, an Authorization string will be attached to the Response Header to return to the Client.
JWTLoginFilter.java
package org.o7planning.sbjwt.filter;
import java.io.IOException;
import java.util.Collections;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.o7planning.sbjwt.service.TokenAuthenticationService;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {
public JWTLoginFilter(String url, AuthenticationManager authManager) {
super(new AntPathRequestMatcher(url));
setAuthenticationManager(authManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.printf("JWTLoginFilter.attemptAuthentication: username/password= %s,%s", username, password);
System.out.println();
return getAuthenticationManager()
.authenticate(new UsernamePasswordAuthenticationToken(username, password, Collections.emptyList()));
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
System.out.println("JWTLoginFilter.successfulAuthentication:");
// Write Authorization to Headers of Response.
TokenAuthenticationService.addAuthentication(response, authResult.getName());
String authorizationString = response.getHeader("Authorization");
System.out.println("Authorization String=" + authorizationString);
}
}
TokenAuthenticationService class is anutility class. It writes "Authorization string" into the Response Header to return to the Client. This Authorization stringworks for a period of time (10 days). This means that the Client needs to log in once and obtains "Authorization string" and can use it in the foresaid period of time. When "Authorization string" expires, the Client has to log in again to obtain a new authorization string.
TokenAuthenticationService.java
package org.o7planning.sbjwt.service;
import java.util.Collections;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
public class TokenAuthenticationService {
static final long EXPIRATIONTIME = 864_000_000; // 10 days
static final String SECRET = "ThisIsASecret";
static final String TOKEN_PREFIX = "Bearer";
static final String HEADER_STRING = "Authorization";
public static void addAuthentication(HttpServletResponse res, String username) {
String JWT = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
.signWith(SignatureAlgorithm.HS512, SECRET).compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);
}
public static Authentication getAuthentication(HttpServletRequest request) {
String token = request.getHeader(HEADER_STRING);
if (token != null) {
// parse the token.
String user = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody()
.getSubject();
return user != null ? new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList()) : null;
}
return null;
}
}
To be able to call the REST API, requests will be attached "Authorization string" on theRequest Header. The JWTAuthenticationFilter class will check "Authorization string". If it is valid, the request will beauthenticated and it may continue to go to the Controller.
JWTAuthenticationFilter.java
package org.o7planning.sbjwt.filter;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.o7planning.sbjwt.service.TokenAuthenticationService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
public class JWTAuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
System.out.println("JWTAuthenticationFilter.doFilter");
Authentication authentication = TokenAuthenticationService
.getAuthentication((HttpServletRequest) servletRequest);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(servletRequest, servletResponse);
}
}
5. Test the application with the browser
No ADS
You can use a browser to test the Login function, and view the operation of JWTLoginFilter class. OK!, Access the following path on the browser:
See the information written on the Console window of the Eclipse:
JWTLoginFilter.attemptAuthentication: username/password= tom,123
JWTLoginFilter.successfulAuthentication:
Authorization String=Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0b20iLCJleHAiOjE1MjA3OTQyNjF9.GP0p5Aaj0HNMShNEAPbieHPeYxeGJ_-lB8ahHr6dJvrs_pAoSgGNCj8bNzRYpi4H7cJ1xQ_DZwV1bMw6ihK2Mw
JWTAuthenticationFilter.doFilter
JWTAuthenticationFilter.doFilter
6. Test the application with RestTemplate
No ADS
Create an example using the RestTemplate class (Spring REST Client) to call a REST API protected by the Auth0:
JWTClientExample.java
package org.o7planning.sbjwt.restclient;
import java.util.Arrays;
import java.util.List;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
public class JWTClientExample {
static final String URL_LOGIN = "http://localhost:8080/login";
static final String URL_EMPLOYEES = "http://localhost:8080/employees";
// POST Login
// @return "Authorization string".
private static String postLogin(String username, String password) {
// Request Header
HttpHeaders headers = new HttpHeaders();
// Request Body
MultiValueMap<String, String> parametersMap = new LinkedMultiValueMap<String, String>();
parametersMap.add("username", username);
parametersMap.add("password", password);
// Request Entity
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(parametersMap, headers);
// RestTemplate
RestTemplate restTemplate = new RestTemplate();
// POST Login
ResponseEntity<String> response = restTemplate.exchange(URL_LOGIN, //
HttpMethod.POST, requestEntity, String.class);
HttpHeaders responseHeaders = response.getHeaders();
List<String> list = responseHeaders.get("Authorization");
return list == null || list.isEmpty() ? null : list.get(0);
}
private static void callRESTApi(String restUrl, String authorizationString) {
// HttpHeaders
HttpHeaders headers = new HttpHeaders();
//
// Authorization string (JWT)
//
headers.set("Authorization", authorizationString);
//
headers.setAccept(Arrays.asList(new MediaType[] { MediaType.APPLICATION_JSON }));
// Request to return JSON format
headers.setContentType(MediaType.APPLICATION_JSON);
// HttpEntity<String>: To get result as String.
HttpEntity<String> entity = new HttpEntity<String>(headers);
// RestTemplate
RestTemplate restTemplate = new RestTemplate();
// Send request with GET method, and Headers.
ResponseEntity<String> response = restTemplate.exchange(URL_EMPLOYEES, //
HttpMethod.GET, entity, String.class);
String result = response.getBody();
System.out.println(result);
}
public static void main(String[] args) {
String username = "tom";
String password = "123";
String authorizationString = postLogin(username, password);
System.out.println("Authorization String=" + authorizationString);
// Call REST API:
callRESTApi(URL_EMPLOYEES, authorizationString);
}
}
No ADS
Spring Boot Tutorials
- Deploy Spring Boot Application on Oracle WebLogic Server
- Create a User Registration Application with Spring Boot, Spring Form Validation
- Spring Boot File Upload Example
- Spring Boot and Groovy Tutorial with Examples
- Spring Boot and MongoDB Tutorial with Examples
- Spring Boot, Hibernate and Spring Transaction Tutorial with Examples
- Spring Boot and Spring Data JPA Tutorial with Examples
- Secure Spring Boot RESTful Service using Auth0 JWT
- Spring Email Tutorial with Examples
- Spring Boot, Apache Tiles, JSP Tutorial with Examples
- Create a Login Application with Spring Boot, Spring Security, JPA
- Use Twitter Bootstrap in Spring Boot
- Spring Tutorial for Beginners
- Spring Boot Interceptors Tutorial with Examples
- Create a Login Application with Spring Boot, Spring Security, Spring JDBC
- Spring Boot File Upload with jQuery Ajax Example
- Spring Boot and JSP Tutorial with Examples
- Install Spring Tool Suite for Eclipse
- Spring Boot, JPA and Spring Transaction Tutorial with Examples
- Spring Boot and Thymeleaf Tutorial with Examples
- Integrating Spring Boot, JPA and H2 Database
- Spring Boot File Upload with AngularJS Example
- Fetch data with Spring Data JPA DTO Projections
- Use Multiple DataSources with Spring Boot and JPA
- CRUD Example with Spring Boot, REST and AngularJS
- Spring Boot and Mustache Tutorial with Examples
- Spring JDBC Tutorial with Examples
- Install a free Let's Encrypt SSL certificate for Spring Boot
- Spring Boot Tutorial for Beginners
- Spring Boot Common Properties
- Spring Boot, Spring JDBC and Spring Transaction Tutorial with Examples
- Use multiple ViewResolvers in Spring Boot
- Deploy Spring Boot Application on Tomcat Server
- Configure Spring Boot to redirect HTTP to HTTPS
- Example of OAuth2 Social Login in Spring Boot
- Create a simple Chat application with Spring Boot and Websocket
- Use Multiple DataSources with Spring Boot and RoutingDataSource
- Spring Boot and FreeMarker Tutorial with Examples
- Create a Shopping Cart Web Application with Spring Boot, Hibernate
- Spring Boot Restful Client with RestTemplate Example
- Secure Spring Boot RESTful Service using Basic Authentication
- Spring Boot File Download Example
- CRUD Restful Web Service Example with Spring Boot
- Create a Multi Language web application with Spring Boot
- Use Logging in Spring Boot
- Run background scheduled tasks in Spring
- Application Monitoring with Spring Boot Actuator
Show More