Angular Spring Boot JWT Authentication Tutorial

author : Sai K

In this tutorial, we will create a JWT (JSON Web Token) authentication system using Spring Boot 3.3 for the

backend and Angular 18 for the frontend. We will use Spring Security for securing the backend, handle CORS

issues, and ensure smooth communication between the Angular frontend and the Spring Boot backend.


Prerequisites

Before we start, ensure you have the following:

  • Java Development Kit (JDK) installed

  • Apache Maven installed

  • Node.js and npm installed

  • Angular CLI installed (npm install -g @angular/cli)

  • An IDE (such as IntelliJ IDEA, Eclipse, or VS Code) installed

Step 1: Setting Up the Spring Boot Backend

1.1 Create a Spring Boot Project

1.Open Spring Initializr:

2.Configure Project Metadata:

  • Project: Maven Project

  • Language: Java

  • Spring Boot: Select the latest version of Spring Boot 3.3

  • Group: com.example

  • Artifact: jwt-auth

  • Name: jwt-auth

  • Description: JWT Authentication Service

  • Package Name: com.example.jwtauth

  • Packaging: Jar

  • Java Version: 17 (or your preferred version)

  • Click Next.
  • 3.Select Dependencies:

  • On the Dependencies screen, select:
    • Spring Web

    • Spring Security

    • Spring Data JPA

    • H2 Database

    • Spring Boot DevTools

  • Click Next.
  • 4.Generate the Project:

    • Click Generate to download the project zip file.

    • Extract the zip file to your desired location.

    5. Extract the zip file to your desired location.

    Open your IDE and import the project as a Maven project.

    1.2 Update application.properties

    Open the application.properties file located in the src/main/resources directory and add the

    following configuration:

    
    server.port=8080
    
    spring.datasource.url=jdbc:h2:mem:testdb
    spring.datasource.driverClassName=org.h2.Driver
    spring.datasource.username=sa
    spring.datasource.password=password
    spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
    spring.h2.console.enabled=true
    spring.jpa.hibernate.ddl-auto=update
    
    jwt.secret=your_jwt_secret_key
    

    Explanation:

    • Configures the server port to 8080.

    • Sets up an in-memory H2 database.

    • Enables the H2 console for database inspection.

    • Automatically updates the database schema based on JPA entities.

    • Adds a secret key for signing JWT tokens.

    1.3 Create User Entity

    Create a User entity class in the com.example.jwtauth.model package:

    
     package com.example.jwtauth.model;
    
    import jakarta.persistence.Entity;
    import jakarta.persistence.GeneratedValue;
    import jakarta.persistence.GenerationType;
    import jakarta.persistence.Id;
    
    @Entity
    public class User {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String username;
        private String password;
        private String email;
    
        // Getters and Setters
    }
    

    Explanation:

    • @Entity: Marks the class as a JPA entity.

    • @Id: Marks the id field as the primary key.

    • @GeneratedValue(strategy = GenerationType.IDENTITY): Configures auto-increment for the id field.

    • username, password, email: Fields representing user attributes.

    • Getters and Setters: Methods to access and modify the fields.

    1.4 Create User Repository

    Create a UserRepository interface in the com.example.jwtauth.repository package:

    
     com.example.jwtauth.repository;
    
    import com.example.jwtauth.model.User;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public interface UserRepository extends JpaRepository {
        User findByUsername(String username);
    }
    

    Explanation:

    • @Repository: Marks the interface as a Spring Data repository.

    • Extends JpaRepository: Provides CRUD operations for the User entity.

    • findByUsername(String username): Custom query method to find a user by username.

    1.5 Create User Service

    Create a UserService class in the com.example.jwtauth.service package that implements UserDetailsService:

    
        package com.example.jwtauth.service;
    
    import com.example.jwtauth.model.User;
    import com.example.jwtauth.repository.UserRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Service;
    
    import java.util.Optional;
    
    @Service
    public class UserService implements UserDetailsService {
    
        private final UserRepository userRepository;
        private final PasswordEncoder passwordEncoder;
    
        @Autowired
        public UserService(UserRepository userRepository) {
            this.userRepository = userRepository;
            this.passwordEncoder = new BCryptPasswordEncoder();
        }
    
        public User saveUser(User user) {
            user.setPassword(passwordEncoder.encode(user.getPassword()));
            return userRepository.save(user);
        }
    
        public Optional findByUsername(String username) {
            return Optional.ofNullable(userRepository.findByUsername(username));
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            User user = userRepository.findByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("User not found");
            }
            return org.springframework.security.core.userdetails.User
                    .withUsername(user.getUsername())
                    .password(user.getPassword())
                    .authorities("USER")
                    .build();
        }
    }
    

    Explanation:

    • @Service: Marks the class as a service component.

    • Implements UserDetailsService: Required by Spring Security to load user-specific data.

    • loadUserByUsername(String username): Loads user details for authentication.

    • saveUser(User user): Saves a new user with an encoded password.

    • findByUsername(String username): Finds a user by username.

    1.6 Create JWT Utility Class

    Create a JwtUtils class in the com.example.jwtauth.util package:

    
        package com.example.jwtauth.util;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.function.Function;
    
    @Component
    public class JwtUtils {
    
        @Value("${jwt.secret}")
        private String secret;
    
        public String extractUsername(String token) {
            return extractClaim(token, Claims::getSubject);
        }
    
        public Date extractExpiration(String token) {
            return extractClaim(token, Claims::getExpiration);
        }
    
        public  T extractClaim(String token, Function claimsResolver) {
            final Claims claims = extractAllClaims(token);
            return claimsResolver.apply(claims);
        }
    
        private Claims extractAllClaims(String token) {
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        }
    
        private Boolean isTokenExpired(String token) {
            return extractExpiration(token).before(new Date());
        }
    
        public String generateToken(UserDetails userDetails) {
            return createToken(userDetails.getUsername());
        }
    
        private String createToken(String subject) {
            return Jwts.builder()
                    .setSubject(subject)
                    .setIssuedAt(new Date(System.currentTimeMillis()))
                    .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
                    .signWith(SignatureAlgorithm.HS256, secret)
                    .compact();
        }
    
        public Boolean validateToken(String token, UserDetails userDetails) {
            final String username = extractUsername(token);
            return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
        }
    }
    

    Explanation:

    • @Component: Marks the class as a Spring bean.

    • Methods for generating and validating JWT tokens.

    • Uses the secret key from the application properties for signing tokens.

    1.7 Create Security Configuration

    Create a SecurityConfig class in the com.example.jwtauth.config package:

    
                    package com.example.jwtauth.config;
    
    import com.example.jwtauth.service.UserService;
    import com.example.jwtauth.util.JwtUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.security.web.SecurityFilterChain;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
    
        private final UserService userService;
        private
    
     final JwtUtils jwtUtils;
    
        @Autowired
        public SecurityConfig(UserService userService, JwtUtils jwtUtils) {
            this.userService = userService;
            this.jwtUtils = jwtUtils;
        }
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/auth/**").permitAll()
                    .anyRequest().authenticated()
                )
                .sessionManagement(session -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .addFilterBefore(new JwtAuthenticationFilter(userService, jwtUtils), UsernamePasswordAuthenticationFilter.class);
    
            return http.build();
        }
    
        @Autowired
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
            return http.getSharedObject(AuthenticationManagerBuilder.class).build();
        }
    }

    Explanation:

    • @Configuration: Marks the class as a configuration component.

    • @EnableWebSecurity: Enables Spring Security.

    • securityFilterChain(HttpSecurity http): Configures HTTP security, disabling CSRF and permitting access to /auth/** endpoints.

    • configure(AuthenticationManagerBuilder auth): Configures the authentication manager with user details service and password encoder.

    • passwordEncoder(): Configures the password encoder

    • authenticationManager(HttpSecurity http): Provides the authentication manager.

    1.8 Create JWT Authentication Filter

    Create a JwtAuthenticationFilter class in the com.example.jwtauth.filter package:

    
        package com.example.jwtauth.filter;
    
    import com.example.jwtauth.service.UserService;
    import com.example.jwtauth.util.JwtUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
    import org.springframework.web.filter.OncePerRequestFilter;
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
        private final UserService userService;
        private final JwtUtils jwtUtils;
    
        @Autowired
        public JwtAuthenticationFilter(UserService userService, JwtUtils jwtUtils) {
            this.userService = userService;
            this.jwtUtils = jwtUtils;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            final String authorizationHeader = request.getHeader("Authorization");
    
            String username = null;
            String jwt = null;
    
            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                jwt = authorizationHeader.substring(7);
                username = jwtUtils.extractUsername(jwt);
            }
    
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userService.loadUserByUsername(username);
    
                if (jwtUtils.validateToken(jwt, userDetails)) {
                    var usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    usernamePasswordAuthenticationToken.setDetails(
                            new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            }
            filterChain.doFilter(request, response);
        }
    }
    

    Explanation:

    • extends OncePerRequestFilter: Ensures that the filter is executed once per request.

    • doFilterInternal(...): Extracts the JWT token from the Authorization header, validates it, and sets the authentication in the security context.

    1.9 Create Authentication Controller

    Create an AuthController class in the com.example.jwtauth.controller package:

    
        package com.example.jwtauth.controller;
    
    import com.example.jwtauth.model.User;
    import com.example.jwtauth.service.UserService;
    import com.example.jwtauth.util.JwtUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    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.core.userdetails.UserDetails;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    @RequestMapping("/auth")
    @CrossOrigin(origins = "http://localhost:4200")
    public class AuthController {
    
        private final AuthenticationManager authenticationManager;
        private final UserService userService;
        private final JwtUtils jwtUtils;
    
        @Autowired
        public AuthController(AuthenticationManager authenticationManager, UserService userService, JwtUtils jwtUtils) {
            this.authenticationManager = authenticationManager;
            this.userService = userService;
            this.jwtUtils = jwtUtils;
        }
    
        @PostMapping("/register")
        public ResponseEntity registerUser(@RequestBody User user) {
            if (userService.findByUsername(user.getUsername()).isPresent()) {
                return ResponseEntity.badRequest().body("Username is already taken");
            }
            userService.saveUser(user);
            return ResponseEntity.ok("User registered successfully");
        }
    
        @PostMapping("/login")
        public ResponseEntity loginUser(@RequestBody User user) {
            try {
                Authentication authentication = authenticationManager.authenticate(
                        new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
                UserDetails userDetails = (UserDetails) authentication.getPrincipal();
                String jwt = jwtUtils.generateToken(userDetails);
                return ResponseEntity.ok(jwt);
            } catch (AuthenticationException e) {
                return ResponseEntity.status(401).body("Invalid username or password");
            }
        }
    }
    

    Explanation:

    • @RestController: Marks the class as a REST controller.

    • @RequestMapping("/auth"): Maps requests to /auth URL.

    • @CrossOrigin(origins = "http://localhost:4200"): Enables CORS for requests from the Angular frontend running on localhost:4200.
    • br
    • registerUser(@RequestBody User user): Handles user registration.

    • loginUser(@RequestBody User user): Handles user login and returns a JWT token.

    Step 2: Setting Up the Angular Frontend

    2.1 Create an Angular Project

    1.Open a terminal and run the following command to create a new Angular project:

    ng new jwt-auth-client

    1.Navigate to the project directory:

    cd jwt-auth-client

    2.2 Install Dependencies

    Install Bootstrap for styling:

    npm install bootstrap

    Add Bootstrap to angular.json:

    
        "styles": [
      "src/styles.css",
      "node_modules/bootstrap/dist/css/bootstrap.min.css"
    ],
    

    2.3 Create Angular Services and Components

    2.3.1 Create Auth Service

    Generate the AuthService:

    ng generate service services/auth

    Edit auth.service.ts:

    
        import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Observable } from 'rxjs';
    
    @Injectable({
      providedIn: 'root'
    })
    export class AuthService {
    
      private baseUrl = 'http://localhost:8080/auth';
    
      constructor(private http: HttpClient) { }
    
      register(user: any): Observable {
        return this.http.post(`${this.baseUrl}/register`, user);
      }
    
      login(credentials: any): Observable {
        return this.http.post(`${this.baseUrl}/login`, credentials);
      }
    }
    

    Explanation:

    • @Injectable({ providedIn: 'root' }): Marks the service as injectable and available throughout the app.
    • HttpClient: Service for making HTTP requests.

    • register(user: any): Sends a POST request to register a new user.

    • login(credentials: any): Sends a POST request to log in a user.

    2.3.2 Create Components

    Generate the components for registration and login:

    
    ng generate component components/register
    ng generate component components/login
        

    Edit register.component.ts:

    
                import { Component } from '@angular/core';
    import { AuthService } from '../../services/auth.service';
    import { Router } from '@angular/router';
    
    @Component({
      selector: 'app-register',
      templateUrl: './register.component.html',
      styleUrls: ['./register.component.css']
    })
    export class RegisterComponent {
      user: any = { username: '', password: '', email: '' };
    
      constructor(private authService: AuthService, private router: Router) { }
    
      register() {
        this.authService.register(this.user).subscribe(() => {
          this.router.navigate(['/login']);
        }, error => {
          console.error('Registration error: ', error);
        });
      }
    } 

    Edit register.component.html:

    
        < div class="container mt-5">
            < div class="row justify-content-center">
              < div class="col-md-6">
                < div class="card">
                  < div class="card-header">Register< /div>
                  < div class="card-body">
                    < form (ngSubmit)="register()">
                      < div class="form-group">
                        < label for="username">Username< /label>
                        < input type="text" class="form-control" id="username" [(ngModel)]="user.username" name="username" required>
                      < /div>
                      < div class="form-group">
                        < label for="email">Email< /label>
                        < input type
          
          ="email" class="form-control" id="email" [(ngModel)]="user.email" name="email" required>
                      < /div>
                      < div class="form-group">
                        < label for="password">Password< /label>
                        < input type="password" class="form-control" id="password" [(ngModel)]="user.password" name="password" required>
                      < /div>
                      < button type="submit" class="btn btn-primary">Register< /button>
                    < /form>
                  < /div>
                < /div>
              < /div>
            < /div>
          < /div>   
    

    Edit login.component.ts:

    
        import { Component } from '@angular/core';
    import { AuthService } from '../../services/auth.service';
    import { Router } from '@angular/router';
    
    @Component({
      selector: 'app-login',
      templateUrl: './login.component.html',
      styleUrls: ['./login.component.css']
    })
    export class LoginComponent {
      credentials: any = { username: '', password: '' };
    
      constructor(private authService: AuthService, private router: Router) { }
    
      login() {
        this.authService.login(this.credentials).subscribe((response) => {
          localStorage.setItem('token', response);
          this.router.navigate(['/home']);
        }, error => {
          console.error('Login error: ', error);
        });
      }
    }

    Edit login.component.html:

    
        < div class="container mt-5">
            <  class="row justify-content-center">
              < div class="col-md-6">
                < div class="card">
                  < div class="card-header">Login< /div>
                  < div class="card-body">
                    < form (ngSubmit)="login()">
                      < div class="form-group">
                        < label for="username">Username < /label>
                        < input type="text" class="form-control" id="username" [(ngModel)]="credentials.username" name="username" required>
                      < /div>
                      < div class="form-group">
                        < label for="password">Password< /label>
                        < input type="password" class="form-control" id="password" [(ngModel)]="credentials.password" name="password" required>
                      < /div>
                       < button type="submit" class="btn btn-primary">Login< /button>
                    < /form>
                  < /div>
                < /div>
              < /div>
            < /div>
          < /div>    
    

    2.4 Update Angular Routing

    Edit app-routing.module.ts:

    
        import { NgModule } from '@angular/core';
    import { RouterModule, Routes } from '@angular/router';
    import { RegisterComponent } from './components/register/register.component';
    import { LoginComponent } from './components/login/login.component';
    
    const routes: Routes = [
      { path: 'register', component: RegisterComponent },
      { path: 'login', component: LoginComponent },
      { path: '', redirectTo: '/register', pathMatch: 'full' }
    ];
    
    @NgModule({
      imports: [RouterModule.forRoot(routes)],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
    

    Explanation:

    • Defines routes for the registration and login components.

    • Redirects the root path to the registration component.

    2.5 Update Angular App Module

    Edit app.module.ts:

    
                        import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { FormsModule } from '@angular/forms';
    import { HttpClientModule } from '@angular/common/http';
    
    import { AppRoutingModule } from './app-routing.module';
    import { AppComponent } from './app.component';
    import { RegisterComponent } from './components/register/register.component';
    import { LoginComponent } from './components/login/login.component';
    
    @NgModule({
      declarations: [
        AppComponent,
        RegisterComponent,
        LoginComponent
      ],
      imports: [
        BrowserModule,
        AppRoutingModule,
        FormsModule,
        HttpClientModule
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule { }   

    Explanation:

    • Imports necessary modules for the Angular app.

    • Declares the components used in the app.

    • Sets up the app's root module.

    2.6 Run the Angular Application

    Open a terminal in the Angular project directory and run the application:

    ng serve

    Visit http://localhost:4200 in your web browser to see the application.

    Conclusion

    In this tutorial, we created a JWT authentication system using Spring Boot 3.3 for the backend and Angular 18 for

    the frontend. We handled CORS issues to ensure smooth communication between the Angular frontend and the

    Spring Boot backend. By following this structure, you can extend and customize the application as needed.


    Related Spring and Spring Boot Tutorials/Guides: