Authentication is the process of verifying the identity of users, ensuring they are who they claim to be before granting access to a system or application. In the context of web applications, this is crucial for protecting sensitive information and resources.
Authentication involves:
For Java developers building Spring-based applications, Spring Security serves as the de-facto standard for securing their creations. This comprehensive framework provides robust authentication and authorization mechanisms, ensuring your application’s data and functionality remain safe from unauthorized access.
JWT stands for JSON Web Token, a self-contained token that contains information about the user and is signed by the server for verification. It has become a popular choice for authorization due to its numerous advantages.
Use Cases: JWTs are widely used in various scenarios:
Public APIs: Securely access public APIs without requiring frequent logins.
Mobile Applications: Store user information and authorization details within the token for offline use.
Single Sign-On (SSO): This allows users to seamlessly access multiple applications with a single login.
Now, we will configure the in-memory user and JWT. We will create an API endpoint and secure it using Spring Boot security.
Use Spring Initializr to create a new Spring Boot project with the following dependencies:
For Web:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
For Security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Lombok:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
For JWT:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
Let’s start with creating an endpoint to be secured:
@RestController
@RequestMapping("/home")
public class Controller {
@Autowired
private UserService userService;
@GetMapping("/users")
public List<User> getUsers(){
System.out.println("getting users");
return userService.getUsers();
}
}
Create a user:
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class User {
private String userId;
private String name;
private String email;
}
Create a service class:
@Service
public class UserService {
private List<User> store=new ArrayList<>();
public UserService() {
store.add(new User(UUID.randomUUID().toString(), "Bhushan", "[email protected]"));
store.add(new User(UUID.randomUUID().toString(), "Ramesh", "[email protected]"));
store.add(new User(UUID.randomUUID().toString(), "Suresh", "[email protected]"));
store.add(new User(UUID.randomUUID().toString(), "Paresh", "[email protected]"));
}
public List<User> getUsers(){
return this.store;
}
}
Create an in-memory user with UserDetailService bean:
@Configuration
public class AppConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.builder().
username("Bhushan")
.password(passwordEncoder().encode("Nemade")).roles("ADMIN").
build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration builder) throws Exception {
return builder.getAuthenticationManager();
}
}
AuthenticationEntryPoint: Used to handle authentication-related exceptions. Specifically, it is responsible for returning an unauthorized (401) response to clients who attempt to access protected resources without proper authentication.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter writer = response.getWriter();
writer.println("Access Denied !! " + authException.getMessage());
}
}
Create a JWT helper class: The helper class is often created to encapsulate the logic related to the generation, parsing, validation, and manipulation of JWTs in a more modular and maintainable way. It provides a set of methods and utilities to interact with JWTs.
@Component
public class JwtHelper {
public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
private String secret = "afafasfafafasfasfasfafacasdasfasxASFACASDFACASDFASFASFDAFASFASDAADSCSDFADCVSGCFVADXCcadwavfsfarvf";
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, secret).compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
Create a JwtAuthenticationFilter class which will extend OncePerRequestFilter: Extending the OncePerRequestFilter
class for a JWT authentication filter in Spring Security is a common approach to ensure that the filter is only executed once per request. This helps prevent unnecessary duplicate processing and ensures that the filter logic is applied consistently.
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private Logger logger = LoggerFactory.getLogger(OncePerRequestFilter.class);
@Autowired
private JwtHelper jwtHelper;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestHeader = request.getHeader("Authorization");
logger.info(" Header : {}", requestHeader);
String username = null;
String token = null;
if (requestHeader != null && requestHeader.startsWith("Bearer")) {
token = requestHeader.substring(7);
try {
username = this.jwtHelper.getUsernameFromToken(token);
} catch (IllegalArgumentException e) {
logger.info("Illegal Argument while fetching the username !!");
e.printStackTrace();
} catch (ExpiredJwtException e) {
logger.info("Given jwt token is expired !!");
e.printStackTrace();
} catch (MalformedJwtException e) {
logger.info("Some changed has done in token !! Invalid Token");
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
} else {
logger.info("Invalid Header Value !! ");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
Boolean validateToken = this.jwtHelper.validateToken(token, userDetails);
if (validateToken) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
logger.info("Validation fails !!");
}
}
filterChain.doFilter(request, response);
}
}
Create a SecurityConfig class as spring security in the configuration file:
@Configuration
public class SecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint point;
@Autowired
private JwtAuthenticationFilter filter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeRequests().
requestMatchers("/test").authenticated().requestMatchers("/auth/login").permitAll()
.anyRequest()
.authenticated()
.and().exceptionHandling(ex -> ex.authenticationEntryPoint(point))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
Create a login API to accept the username and password:
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationManager manager;
@Autowired
private JwtHelper helper;
private Logger logger = LoggerFactory.getLogger(AuthController.class);
@PostMapping("/login")
public ResponseEntity<JwtResponse> login(@RequestBody JwtRequest request) {
this.doAuthenticate(request.getEmail(), request.getPassword());
UserDetails userDetails = userDetailsService.loadUserByUsername(request.getEmail());
String token = this.helper.generateToken(userDetails);
JwtResponse response = JwtResponse.builder()
.jwtToken(token)
.username(userDetails.getUsername()).build();
return new ResponseEntity<>(response, HttpStatus.OK);
}
private void doAuthenticate(String email, String password) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(email, password);
try {
manager.authenticate(authentication);
} catch (BadCredentialsException e) {
throw new BadCredentialsException(" Invalid Username or Password !!");
}
}
@ExceptionHandler(BadCredentialsException.class)
public String exceptionHandler() {
return "Credentials Invalid !!";
}
}
Create a JWT Request and JWT Response to receive data and send login details: Creating a JWT request and response involves defining structures for sending and receiving JWTs during the authentication process. Typically, a JWT request is used to send user credentials (e.g., username and password) to the server for authentication, and a JWT response is used to deliver a token upon successful authentication.
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class JwtRequest {
private String email;
private String password;
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@ToString
public class JwtResponse {
private String jwtToken;
private String username;
}
Test the API using Postman to generate a JWT token, then use that token as a header in subsequent requests to access the protected API and verify successful authentication.
Dive Deeper :
Thank you for reading😊
Also published here.