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:
- Go to Spring Initializr in your web browser.
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)
3.Select Dependencies:
- Spring Web
- Spring Security
- Spring Data JPA
- H2 Database
- Spring Boot DevTools
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.