Angular Spring Boot JWT Authentication Tutorial

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. Open the Project in Your IDE:

    • 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:

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, Long> {
    User findByUsername(String username);
}

Explanation:

  • @Repository: Marks the interface as a Spring Data repository.
  • Extends JpaRepository<User, Long>: 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<User> 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> T extractClaim(String token, Function<Claims, T> 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.
  • 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<any> {
    return this.http.post(`${this.baseUrl}/register`, user);
  }

  login(credentials: any): Observable<any> {
    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">
  <div 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.

Comments