What is Authentication? π
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:
- User Credentials:π Users provide credentials, such as usernames and passwords.
- Verification Process:βοΈ The system verifies the provided credentials against stored data. Successful verification grants access; otherwise, access is denied.
Spring Securityπ
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 (JSON Web Token)Authentication: A Secure and Scalable Approachππ
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, let's delve into the practical application of JWT by implementing it in a Spring Boot application to secure our API endpoints.βπ·
Now we will configure the in-memory user and JWT. We will create an API endpoint and secure it using Spring Boot security.
Create a Spring Boot Project
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", "bhushan@gmail.com"));
store.add(new User(UUID.randomUUID().toString(), "Ramesh", "ramesh@gmail.com"));
store.add(new User(UUID.randomUUID().toString(), "Suresh", "suresh@gmail.com"));
store.add(new User(UUID.randomUUID().toString(), "Paresh", "paresh@gmail.com"));
}
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();
}
}
Start with creating class JWTAthenticationEntryPoint that will be implementing 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 :
https://www.youtube.com/watch?v=q2l91Ffc_8U&t=3299s
https://springframework.guru/jwt-authentication-in-spring-microservices-jwt-token/
Thank you for readingπ
Top comments (2)
Building user authentication and authorization oneself, as part of the development of a project, is an option. And articles like this are a fantastic resource for showing you how; nice work @bhushan_n π However building user authentication and authorization when it's not ones core focus can lead to a whole lot of unnecessary work - that can also open an application to some unwanted and undesirable security implications. Check out my DEV post here to see how integrating with a SaaS platform - such as Auth0 - can be beneficial for a whole host of reasons π€
Sigh, yet another post about adding authentication to your app, and doing so with JWT, given visibility by DEV on social networks.
I was tired if it and wrote a couple posts about it (and I'll tirelessly post them in comments of such posts):