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.
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.
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>
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;
}
}
###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;
}
## 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");
}
}
###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;
}
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();
}
## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9ucl4y34q01y94tjcrql.jpg)
public login(payload: MfaVerificationResponse): void {
if(payload.tokenValid && !payload.mfaRequired){
localStorage.clear();
localStorage.setItem(this.tokenKey, payload.jwt);
}
}
###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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h8hjztmcgt63vvnkle4t.jpg)
> Jwt Token Interceptor Implementation
@Injectable({
providedIn: 'root'
})
export class TokenInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthService) { }
intercept(
request: HttpRequest,
next: HttpHandler
): Observable> {
if (this.authenticationService.isLoggedIn()) {
let newRequest = request.clone({
setHeaders: {
Authorization: Bearer ${this.authenticationService.getToken()}
,
},
});
return next.handle(newRequest);
}
return next.handle(request);
}
}
###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,
next: HttpHandler
): Observable> {
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){
return request.url.includes("/login") || request.url.includes("/verifyTotp");
}
}
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']);
}
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);
}
}
> Home HTML
Welcome!!
{{message}}
## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/noolbz198cbnmviybajh.png)
![200 OK from backend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/876iugt53lf4rsshmcza.png)
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
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)