Introduction
In this tutorial, we’ll create a React application and a login form. We’ll use this login form with Spring Boot application with security to login the application.
Create Spring Boot Project
Following is the Spring Boot project structure:
src/ ├── main/ │ ├── java/com/example/demo/ │ │ ├── config/ │ │ │ └── SecurityConfig.java │ │ ├── controller/ │ │ │ └── AuthController.java │ │ ├── model/ │ │ │ └── LoginRequest.java │ │ └── DemoApplication.java │ └── resources/ │ └── application.yml
SecurityConfig.java
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.User; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(authz -> authz .requestMatchers("/api/auth/login").permitAll() .anyRequest().authenticated() ) .formLogin(login -> login.disable()); return http.build(); } @Bean public InMemoryUserDetailsManager userDetailsService() { UserDetails user = User.withUsername("john") .password(passwordEncoder().encode("secret")) .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } }
LoginRequest.java
public class LoginRequest { private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
AuthController.java
import com.oauth.sample.model.LoginRequest; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/auth") @CrossOrigin(origins = "*") public class AuthController { private final AuthenticationManager authenticationManager; public AuthController(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @PostMapping("/login") public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) { try { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword() ) ); SecurityContextHolder.getContext().setAuthentication(authentication); return ResponseEntity.ok("Login successful"); } catch (AuthenticationException ex) { return ResponseEntity.status(401).body("Invalid credentials"); } } }
Create a React application
Create your React application. Following are the important components:
App.tsx
import React from "react"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import LoginForm from "./LoginForm"; import Dashboard from "./Dashboard"; const App: React.FC = () => { return ( <Router> <Routes> <Route path="/" element={<LoginForm />} /> <Route path="/dashboard" element={<Dashboard />} /> </Routes> </Router> ); }; export default App;
LoginForm.tsx
import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; function LoginForm() { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const navigate = useNavigate(); const handleLogin = async (e) => { e.preventDefault(); try { const response = await fetch("http://localhost:8080/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username, password }), }); if (response.ok) { // ✅ Redirect to dashboard after successful login navigate("/dashboard"); } else { const msg = await response.text(); setError(msg || "Login failed"); } } catch (err) { setError("Error connecting to server"); } }; return ( <div className="login-container"> <h2>Login</h2> {error && <p style={{ color: "red" }}>{error}</p>} <form onSubmit={handleLogin}> <div> <label>Username: </label> <input value={username} onChange={(e) => setUsername(e.target.value)} required /> </div> <div> <label>Password: </label> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required /> </div> <button type="submit">Login</button> </form> </div> ); } export default LoginForm;
Dashboard.jsx
import React from "react"; function Dashboard() { return ( <div> <h1>Welcome to the Dashboard!</h1> <p>You have successfully logged in.</p> </div> ); } export default Dashboard;
Now, run both your applications.
Now try to login the application with username as ‘john’ and password as ‘secret’. And one successfully logged in, you will be redirected to the ‘dashboard’ page.
How this works?
Disable CSRF
.csrf(csrf -> csrf.disable())
- CSRF (Cross Site Request Forgery) protection is disabled here.
- This is safe for a REST API consumed by a frontend like React (especially when you’re not using cookies or sessions).
- CSRF is mainly for forms submitted from browsers—since this app is using token-based or stateless login, we can safely disable it.
Authorization Rules
.authorizeHttpRequests(authz -> authz .requestMatchers("/api/auth/login").permitAll() .anyRequest().authenticated() )
- This section sets up URL access rules.
.requestMatchers("/api/auth/login").permitAll()
:- Allows unauthenticated access to the
/api/auth/login
endpoint.
- Allows unauthenticated access to the
.anyRequest().authenticated()
:- All other endpoints require the user to be logged in.
So the login endpoint is open to everyone, while everything else is protected.
Disable Form Login
.formLogin(login -> login.disable());
- Disables Spring Security’s default HTML login form.
- Since you’re using a custom login page (in React), you don’t need the default form provided by Spring Security.
Authentication
UsernamePasswordAuthenticationToken
is a Spring Security object that holds the username and password the user submitted.
You wrap these credentials like this:
new UsernamePasswordAuthenticationToken(username, password)
authenticationManager.authenticate(…)
This line triggers the real authentication logic:
- It passes the token to the authentication manager, which checks:
- Is this username present in the system?
- Does the submitted password match the stored password (after encoding)?
Behind the scenes, this uses the InMemoryUserDetailsManager
you defined in SecurityConfig.java
.
If credentials are valid:
- Returns an authenticated
Authentication
object. - Sets it into the SecurityContext (so the user is considered logged in).
- Returns
"Login successful"
to the frontend.
If credentials are invalid:
- An
AuthenticationException
is thrown. - You catch it and return a
401 Unauthorized
.
In SecurityConfig.java
, you declared:
@Bean public InMemoryUserDetailsManager userDetailsService() { UserDetails user = User.withUsername("john") .password(passwordEncoder().encode("secret")) .roles("USER") .build(); return new InMemoryUserDetailsManager(user); }
So Spring Security:
- Looks up the username
john
in this in-memory store. - Compares the password (
secret
) using the BCrypt encoder. - If matched, authentication succeeds.
SecurityContextHolder.getContext()
- This gets the current thread’s security context.
- Think of the SecurityContext as a container that holds security-related information for the current user (like authentication status, roles, etc.).
.setAuthentication(authentication)
- You’re setting the authenticated user object (returned by the
AuthenticationManager
) into this context. - This means Spring now knows:
- Who the current user is
- That they are authenticated
- What their roles/authorities are