DEV Community

Cover image for Secure Your Spring Boot and Angular Application with JWT Authentication: A Comprehensive Guide
Anbumani
Anbumani

Posted on

Secure Your Spring Boot and Angular Application with JWT Authentication: A Comprehensive Guide

In the world of web development, security is a critical aspect that cannot be overlooked. This blog post will guide you through the process of securing your Spring Boot backend and Angular frontend using JSON Web Tokens (JWT) for authentication. We'll cover the generation and validation of JWT on the server side, as well as implement the necessary features on the Angular side to handle authentication, error interception, and token storage.

Understanding JWT and Its Significance

JSON Web Tokens (JWT) have emerged as a key element in securing modern web applications, offering a compact and efficient way to transmit information between parties. Below is a brief overview of JWT and its pivotal role in contemporary web development:

What is JWT?

JWT is an open standard that defines a self-contained method for securely transmitting information as a JSON object. JWTs are commonly used for authentication, authorization, and secure communication by comprising a header, payload, and signature.

Components of a JWT:

Header: Specifies the token type and signing algorithm.
Payload: Contains claims and additional data.
Signature: Ensures the token's integrity and is created by combining the encoded header, payload, and a secret key.

Components of a JWT

Role of JWT in Modern Web Applications:

Stateless Authentication: JWTs eliminate the need for server-side session storage, allowing stateless authentication in scalable systems.

Authorization: JWTs carry user claims, aiding servers in making access control decisions.

Inter-Service Communication: JWTs facilitate secure communication between microservices, ensuring authenticity and authorization.

Compact and Efficient: The concise format of JWTs makes them ideal for transmitting information efficiently.

Advantages of JWT:

Security: JWTs can be signed and encrypted for integrity and confidentiality.

Decentralized: JWTs are self-contained, reducing the need for constant communication with a central authority.

Cross-Domain Compatibility: JWTs can be easily transmitted across different domains and are widely supported.

Considerations:

Token Expiry: JWTs can have an expiration time for enhanced security.

Sensitive Information: Avoid including highly sensitive information in the payload to minimize security risks.

thus, JWTs serve as a versatile and secure solution for authentication, authorization, and information exchange in modern web applications, contributing to the evolving landscape of web development.

Setting Up Spring Boot for JWT Authentication

Create an application setup with spring security. You can refer to my previous post to set up one.

Implementing JWT Service.

After successful authentication, the application will generate and sign the JWT with claims, expiration, and other parameters. Below is the flow that explains the JWT token generation.

JWT Token Generation

Let's configure the dependencies below for JWT support. This will help us in JWT generation and validation.

<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>
Enter fullscreen mode Exit fullscreen mode

As a next step, we will create a service that will generate and validate JWT.

io.jsonwebtoken.Jwts provides a handy builder method to build the JWT.

Let's create a secret key using the HMACSHA256 algorithm. This key will be used to ensure the integrity of JWT.

JWT Generate method

With the builder method let's set the issuer of JWT, Subject, Custom claim "username", and the expiration. Finally, sign the JWT with the secret key.

JWT Validate Method

We will use the JWT parser using the secret key to parse the claims from the JWT.

The code below shows the complete JWT service implementation

@Service
public class JWTServiceImpl implements JWTService {

    private final String key = "jxgEQeXHuPq8VdbyYFNkANdudQ53YUn4";
    private final SecretKey secretKey = Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));

    @Override
    public String generateJwt(String username) throws ParseException {
        Date date= new Date();
        return  Jwts.builder()
                .setIssuer("MFA Server")
                .setSubject("JWT Auth Token")
                .claim("username", username)
                .setIssuedAt(date)
                .setExpiration(new Date(date.getTime() + 60000))
                .signWith(secretKey)
                .compact();
    }

    @Override
    public Authentication validateJwt(String jwt) {
        JwtParser jwtParser = Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build();
        Claims claims = jwtParser.parseClaimsJws(jwt).getBody();
        String username = (String)claims.getOrDefault("username",null);
        if(Objects.nonNull(username)){
            return new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
        }
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

Configuring Spring Security for JWT validation.

To validate JWT we will create a custom filter that retrieves the token from incoming requests and sets the authorization token to the security context. Also, We will create an Exception handler that responds with 401 went the user is not authenticated.

JWT Validation Filter

This filter will be configured to execute before the "UsernameAndPasswordAuthenticvationFilter". This filter extends the OncePerRequestFiolter and provides the implantation to doFilter.

We will validate the JWt from the request by extracting the authorization header and using our JWT service to validate and set the authentication token to the security context. Please see the implementation below.

public JwtValidationFilter(JWTService jwtService) {
        this.jwtService = jwtService;
    }


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            //retrieve token
            String jwt = getJWT(request);
            if (Objects.nonNull(jwt)) {
                // Validate the JWT from the Request
                UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) jwtService.validateJwt(jwt);
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }catch (Exception e){
            log.error("Exception while processing the JWT"+e.getMessage());
        }
        filterChain.doFilter(request, response);
    }

    private String getJWT(HttpServletRequest request){
        String jwt = request.getHeader("authorization");
        if(Objects.nonNull(jwt) && jwt.startsWith("Bearer") &&
        jwt.length()>7){
            return jwt.substring(7);
        }
        return null;
    }
Enter fullscreen mode Exit fullscreen mode

Exception Handler

When there is any exception in the process of authentication we will respond with 401 - Unauthorized error. This is achieved using the Authentication Entry point. Where we will configure the exception handler. Look at the simple implementation below.

@Component
@Slf4j
public class AuthExceptionHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.error("Unauthorized {}", authException.getMessage());
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "User is not Authenticated");
    }
}
Enter fullscreen mode Exit fullscreen mode

Managing Cross-Origin Resource Sharing (CORS) to ensure secure communication.

Our angular application is running on a different port than the back end. it is considered a different domain. So we need to advise Spring Security to share data to the Cross Origins.

Let's use the CORS configuration to set allowed Origins, Methods, and headers. As we are expecting authorization and content-type header let's add them. For any URL in the application add this configuration using UrlBasedCorsConfigurationSource object.

 @Bean
    CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
        configuration.setAllowedMethods(Arrays.asList("GET","POST"));
        configuration.setAllowedHeaders(Arrays.asList("authorization","content-type"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**",configuration);
        return source;
    }
Enter fullscreen mode Exit fullscreen mode

Finally, we add this to the security configuration.

@Bean
    public SecurityFilterChain defaultFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .cors(cors-> cors.configurationSource(corsConfigurationSource()))
                .csrf(csrf-> csrf.disable())
                .exceptionHandling(handle -> handle.authenticationEntryPoint(authExceptionHandler))
                .addFilterBefore(jwtValidationFilter, UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(session-> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth-> auth
                        .requestMatchers("/error**","confirm-email","/register**","/login**","/verifyTotp**").permitAll()
                        .anyRequest().authenticated()
                )
               .build();
    }
Enter fullscreen mode Exit fullscreen mode

Angular Setup for JWT Authentication

Storing JWT in Local Storage

On successful authentication, we will add the JWT to local storage and further, it will be used by the application for backend API calls.

Store JWT

public login(payload: MfaVerificationResponse): void {
    if(payload.tokenValid && !payload.mfaRequired){
      localStorage.clear();
      localStorage.setItem(this.tokenKey, payload.jwt);
    }
  }
Enter fullscreen mode Exit fullscreen mode

Adding JWT to outgoing requests for secure communication with the backend.

HTTP Interceptors from angular allow us to intercept all the requests and responses. We will create two interceptors one to add the authorization token to the request header and another to redirect user to login page if the user is unauthorized.

Interceptor Workflow

Jwt Token Interceptor Implementation

@Injectable({
  providedIn: 'root'
})
export class TokenInterceptor implements HttpInterceptor {

  constructor(private authenticationService: AuthService) { }


  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (this.authenticationService.isLoggedIn()) {
      let newRequest = request.clone({
        setHeaders: {
          Authorization: `Bearer ${this.authenticationService.getToken()}`,
        },
      });
      return next.handle(newRequest);
    }
    return next.handle(request);
  }
}
Enter fullscreen mode Exit fullscreen mode

Setting up an error interceptor for better error handling.

Error handler will skip the login page from 401 validation. If the backend responds with 401 for any API call then it is assumed the user should log in again to get access. We can customize this when we have role-level access.

@Injectable({
  providedIn: 'root'
})
export class ErrorInterceptor implements HttpInterceptor {

  constructor(private authenticationService: AuthService) { }


  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {

    return next.handle(request).pipe(catchError(error=>{
      if(error.status == 401 && !this.isLoginPage(request)){
        this.authenticationService.logout();
      }
      const errMsg = error.error.message || error.statusText;
      return throwError(()=> errMsg);
    }));
  }

  private isLoginPage(request: HttpRequest<any>){
    return request.url.includes("/login") || request.url.includes("/verifyTotp");
  }
}

Enter fullscreen mode Exit fullscreen mode

Along the logout, we will clear the token from local storage and navigate the user to login page.

  public logout() {
    localStorage.removeItem(this.tokenKey);
    this.router.navigate(['/login']);
  }
Enter fullscreen mode Exit fullscreen mode

Now let's call a protected URL on the Home page load.

export class HomeComponent implements OnInit {
  message: string="";
  constructor(private homeService: HomeService) { }

  ngOnInit(): void {
    this.homeService.getProtectedString().subscribe(s=> this.message =s);
  }

}
Enter fullscreen mode Exit fullscreen mode

Home HTML

<div class="container-fluid">
<div class="card">
    <div class="card-header">
        <h1>Welcome!!</h1>
    </div>
    <div class="card-body">
        <h2>{{message}}</h2>
    </div>
</div>
</div>
Enter fullscreen mode Exit fullscreen mode

Let's execute the code and check it out.

After successful login, you can see the bearer token is added to the header and we have received the response.

UI Home Page
200 OK from backend

We have successfully secured the Spring boot and Angular application using JWT. Great for reading till the end. Check out the GitHub repository & do comment if have any questions, I am happy to answer all.

Backend code

GitHub logo amaialth / mfaserver

Spring boot backend for MFA

MFA Server

Application for 2FA demo.

APIs Involved

  • login
  • register
  • verifyTotp
  • confrim-email

Dependencies

  • Spring Security
  • Spring Web
  • dev.samstevens.totp

Angular UI

Mfaapplication

Application developed using Angular 14 and Bootstrap.

Components involved.

  • Login
  • Register
  • TOTP
  • Home Module



Top comments (0)