Spring Security Fundamentals & Basic Authentication

Business Scenario

In this lab, we will enhance the Medicare backend by integrating Spring Security to protect REST APIs and authenticate users using email and password.

Pre-Lab Preparation

Phase 8 —  Spring Security Fundamentals + Basic Authentication

Authentication vs Authorization

1

  • Before writing code, this distinction needs to be completely clear.

Module:

1) Mastering Springboot Security​​

Authentication

Authorization

Question: "Who are you?"

Question: "What can you do?

Purpose: Confirms identity.

Purpose: Grants or denies permissions.

Example:

Example:

Email: rahul@gmail.com
Password: Rahul@123
  • Spring checks whether those credentials are correct.
  • Result:

Rahul is authenticated

Rahul
Role = PATIENT
  • Can Rahul:

View Doctors?

Book Appointment

Create Prescription

  • Open: pom.xml  and add

Add Spring Security Dependency

2

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

What Happens Immediately?

  • If you restart Spring Boot immediately after adding Spring Security, most endpoints become protected automatically.
  • For example:

GET /api/doctors

will no longer behave like before.

  • That's because Spring Security places filters before the MVC controller. Spring Security's servlet support is fundamentally filter-based. ​
Postman
   ↓
Security Filters
   ↓
DispatcherServlet
   ↓
Controller
  • Conceptually:
  • Until now, we deliberately didn't have a password because we hadn't learned password security.

Add Password to User

3

@NotBlank(message = "Password is required")
@Size(min = 8,max = 100,message = "Password must be between 8 and 100 characters)
@Column(nullable = false)
private String password;
  • Now we have reached the correct phase.
  • Update: entity/User.java
  • Add :
  • Until now, we deliberately didn't have a password because we hadn't learned password security.

Add Password to User

4

@NotBlank(message = "Password is required")
@Size(min = 8,max = 100,message = "Password must be between 8 and 100 characters)
@Column(nullable = false)
private String password;
  • Now we have reached the correct phase.
  • Update: entity/User.java
  • Add :
public String getPassword() {
    return password;
}

public void setPassword(String password) {
    this.password = password;
}
  • Add getter/setter:

Important Database Note

  • Your existing users table contains records created before the password column existed.
  • Because we're still using development/test data, the cleanest beginner-friendly approach at this stage is to reset the development database.
  • Execute the following SQL commands:
DROP DATABASE medicare_db;
CREATE DATABASE medicare_db;
  • Then restart Spring Boot and let Hibernate create the tables again.

Note: We are dropping and recreating the development database because the User entity now includes a new password field required for Spring Security. This allows Hibernate to generate the updated table structure. This will permanently delete all existing data, so proceed only if the data is disposable.

Passwords Must NEVER Be Stored Directly

  • Never do:

password = Rahul@123

inside the database.

email              password
--------------------------------
rahul@gmail.com    Rahul@123
  • Instead we store something similar to:
$2a$10$...

using: BCrypt

  • Spring Security's PasswordEncoder abstraction is what DaoAuthenticationProvider uses when checking username/password credentials.

Create Security Package

5

com.medicare/
└── security

We Now Need User DTOs

6

  • Until now returning User directly wasn't a huge problem, but now it is.
  • User contains: password and We must NEVER return that to the frontend.
  • So this is exactly the kind of situation where our project rule says: DTO is necessary.
dto/
├── UserRequest.java
└── UserResponse.java
  • Create :

Create UserRequest

7

package com.medicare.dto;

import com.medicare.entity.Role;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class UserRequest {

    @NotBlank(message = "Name is required")
    private String name;

    @NotBlank(message = "Email is required")
    @Email(message = "Please provide a valid email")
    private String email;

    @NotBlank(message = "Phone number is required")
    private String phone;

    @NotBlank(message = "Password is required")
    @Size(
            min = 8,
            max = 100,
            message = "Password must be between 8 and 100 characters"
    )
    private String password;

    @NotNull(message = "Role is required")
    private Role role;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }
}
    @NotBlank(message = "Name is required")
    private String name;

    @NotBlank(message = "Email is required")
    @Email(message = "Please provide a valid email")
    private String email;

    @NotBlank(message = "Phone number is required")
    private String phone;

    @NotBlank(message = "Password is required")
    @Size(
            min = 8,
            max = 100,
            message = "Password must be between 8 and 100 characters"
    )
    private String password;

    @NotNull(message = "Role is required")
    private Role role;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Role getRole() {
        return role;
    }
 public void setRole(Role role) {
        this.role = role;
    }
}

Create UserResponse

8

package com.medicare.dto;
import com.medicare.entity.Role;

public class UserResponse {
    private Long id;
    private String name;
    private String email;
    private String phone;
    private Role role;
    
    public UserResponse(
            Long id,
            String name,
            String email,
            String phone,
            Role role) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.phone = phone;
        this.role = role;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public String getPhone() {
        return phone;
    }

    public Role getRole() {
        return role;
    }
}
  • Notice there is NO password here.
  • So:
DATABASE ENTITY
User
 ├── name
 ├── email
 ├── phone
 ├── password 
 └── role
API RESPONSE
UserResponse
 ├── name
 ├── email
 ├── phone
 └── role

Update UserRepository

9

  • We need to authenticate users by email.
  • Add:
Optional<User> findByEmailIgnoreCase(
        String email
);
  • Email becomes our security: username In Spring Security terminology.

Create PasswordEncoder

10

  • Create package config and inside it : Create PasswordEncoder
config/
├── SecurityConfig.java


package com.medicare.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • This creates a Spring-managed: PasswordEncodethat other classes can use.

Update UserService

11

  • Inject:PasswordEncoder into the service
  • Our create-user logic must now:
Receive plain password
       ↓
BCrypt encode
       ↓
Store encoded password
  • Update relevant parts of UserService:
package com.medicare.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.medicare.dto.UserRequest;
import com.medicare.dto.UserResponse;
import com.medicare.entity.User;
import com.medicare.exception.UserNotFoundException;
import com.medicare.repository.UserRepository;
@Service
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public UserService(
            UserRepository userRepository,
            PasswordEncoder passwordEncoder) {

        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    public UserResponse createUser(
            UserRequest request) {

        if (userRepository.existsByEmail(
                request.getEmail())) {

            throw new IllegalArgumentException(
                    "Email is already registered"
            );
        }

        User user = new User();

        user.setName(request.getName());
        user.setEmail(request.getEmail());
        user.setPhone(request.getPhone());
        /*
         * Never store a plain-text password.
         * BCrypt converts it into a secure hash.
         */
        user.setPassword(
                passwordEncoder.encode(
                        request.getPassword()
                )
        );

        user.setRole(request.getRole());

        User savedUser = userRepository.save(user);

        return mapToUserResponse(savedUser);
    }

    public List<UserResponse> getAllUsers() {

        List<User> users =
                userRepository.findAll();

        List<UserResponse> responses =
                new ArrayList<>();

        for (User user : users) {

            responses.add(mapToUserResponse(user));
        }
        return responses;
    }

    public UserResponse getUserById(Long id) {

        User user = userRepository
                .findById(id)
                .orElseThrow(() ->
                        new UserNotFoundException(
                                "User not found with id: " + id
                        )
                );

        return mapToUserResponse(user);
    }

    private UserResponse mapToUserResponse(
            User user) {

        return new UserResponse(
                user.getId(),
                user.getName(),
                user.getEmail(),
                user.getPhone(),
                user.getRole()
        );
    }
}

Update UserController

12

  • Now use the DTOs:
@PostMapping
public ResponseEntity<UserResponse>
        createUser( @Valid @RequestBody UserRequest request) {

    UserResponse response = userService.createUser(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
  • GET methods should also return: UserRespons rather than:User
  • This guarantees: password never appears in API responses .

Create CustomUserDetailsService

13

package com.medicare.security;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.medicare.entity.User;
import com.medicare.repository.UserRepository;

@Service
public class CustomUserDetailsService
        implements UserDetailsService {
  • Now we connect our database user to Spring Security.
  • Create 
security/
└── CustomUserDetailsService.java
    private final UserRepository userRepository;

    public CustomUserDetailsService( UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(
            String username)
            throws UsernameNotFoundException {

        User user = userRepository
                .findByEmailIgnoreCase(username)
                .orElseThrow(() ->
                        new UsernameNotFoundException(
                                "User not found with email: "
                                        + username
                        )
                );

        return org.springframework.security
                .core.userdetails.User
                .withUsername(user.getEmail())
                .password(user.getPassword())
                .roles(user.getRole().name())
                .build();
    }
}
  • UserDetailsService has one core job: load user-specific data by username; DaoAuthenticationProvider then uses it during username/password authentication.

Why Do We Have Two User Classes?

We have: com.medicare.entity.User Our database entity. And Spring provides -org.springframework.security.core.userdetails.User Spring Security representation.

  • That's why we used : org.springframework.security.core.userdetails.User fully qualified in the builder.

What Does .roles() Do?

  • We write: .roles(user.getRole().name())
  • Suppose: Role = PATIENT Spring turns it conceptually into: ROLE_PATIENT
  • Authorities are stored on the authenticated Authentication object and later used by authorization components. ​

Create DaoAuthenticationProvider

14

  • Now expand : SecurityConfig.java
  • Add :
@Bean
public AuthenticationProvider authenticationProvider(
        CustomUserDetailsService userDetailsService,
        PasswordEncoder passwordEncoder) {

    DaoAuthenticationProvider provider = 
          new DaoAuthenticationProvider(userDetailsService);
    provider.setPasswordEncoder( passwordEncoder);
    return provider;
}
  • Imports:
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import com.medicare.security.CustomUserDetailsService;
  • The current Spring Security API provides a DaoAuthenticationProvider(UserDetailsService)  constructor and lets us configure the PasswordEncoder.
  • Add AuthenticationManager
@Bean
public AuthenticationManager authenticationManager(
          AuthenticationConfiguration configuration) throws Exception {
    return configuration.getAuthenticationManager();
}
  • Imports:
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.
AuthenticationConfiguration;
HTTP Request
     ↓
Spring Security
     ↓
AuthenticationManager
     ↓
ProviderManager
  • Architecture So Far
 
 
     ↓
DaoAuthenticationProvider
     ↓
CustomUserDetailsService
     ↓
UserRepository
     ↓
MySQL
     ↓
PasswordEncoder
     ↓
Authentication Result

Configure SecurityFilterChain

15

  • Now add the main security configuration.
  • Your final SecurityConfig for this phase can look like:
 
 
package com.medicare.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.
AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import com.medicare.security.CustomUserDetailsService;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder();
    }
 
 
    @Bean
    public AuthenticationProvider authenticationProvider(
            CustomUserDetailsService userDetailsService,
            PasswordEncoder passwordEncoder) {

        DaoAuthenticationProvider provider =
                new DaoAuthenticationProvider(
                        userDetailsService
                );

        provider.setPasswordEncoder(
                passwordEncoder
        );

        return provider;
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration configuration)
            throws Exception {

        return configuration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(
            HttpSecurity http,
            AuthenticationProvider authenticationProvider)
 
 
            throws Exception {

        http
                /*
                 * For this REST/Postman learning phase,
                 * requests are authenticated with HTTP Basic.
                 */
                .csrf(csrf -> csrf.disable())

                .authenticationProvider(authenticationProvider)

                .authorizeHttpRequests(auth ->
                        auth

                                // Health check is public
                                .requestMatchers(
                                        "/api/health"
                                )
                                .permitAll()

                                /*
                                 * Temporarily public so we can
                                 * create test users.
                                 *
                                 * Phase 9 will move registration
                                 * into /api/auth/register.
                                 */
                                .requestMatchers(HttpMethod.POST,"/api/users")
                                .permitAll()
 
 
                                // Everything else requires login
                                .anyRequest()
                                .authenticated()
                )

                .httpBasic(
                        Customizer.withDefaults()
                );

        return http.build();
    }
}

Important Note About CSRF

  • Spring Security enables CSRF protection by default for unsafe requests such as POST.
  • For this Postman-based REST training phase, we're disabling it so we can focus on HTTP Basic authentication without also introducing CSRF tokens.

Create Our First Secured User

16

  • Restart the application.
  • Create:
POST
http://localhost:8080/api/users
  • This endpoint is temporarily public.
  • Body:
{
    "name": "Rahul Shah",
    "email": "rahul@example.com",
    "phone": "9876543210",
    "password": "Rahul@123",
    "role": "PATIENT"
}
  • Expected: 201 CREATED
  • Response:
{
    "id": 1,
    "name": "Rahul Shah",
    "email": "rahul@example.com",
    "phone": "9876543210",
    "role": "PATIENT"
}
  • Notice: passworddoesn't appear.

Check MySQL

17

  • Run:
SELECT id, name, email, password, role FROM users;
  • You should see something conceptually like:
id:       1
name:     Rahul Shah
email:    rahul@example.com
password: $2a$10$...................
role:     PATIENT
  • You should NOT see: Rahul@123 ->Password encoding works.

Try Protected API Without Authentication

18

  • Postman:
GET
http://localhost:8080/api/doctors
  • Do not provide credentials.
  • Expected: 401 UNAUTHORIZED
  • Meaning: You have not successfully authenticated.

Configure Basic Auth in Postman

19

  • Go to:
Authorization
      ↓
Type
      ↓
Basic Auth
  • Enter:
Username: rahul@example.com
Password: Rahul@123
  • Postman generates the appropriate: Authorization: Basic ...  header.

Use Wrong Password

20

  • Use:
Username: rahul@example.com
Password: WrongPassword
  • Call:
GET /api/doctors
  • Expected: 401 UNAUTHORIZED

Create Doctor User

21

  • Create another test user:
POST /api/users
{
    "name": "Dr. Ananya Sharma",
    "email": "ananya@example.com",
    "phone": "9876543211",
    "password": "Doctor@123",
    "role": "DOCTOR"
}
  • Now our database contains:
Rahul : PATIENT
Dr. Ananya : DOCTOR
  • Both can currently authenticate. We haven't restricted their actions differently yet.
SecurityContextHolder
       ↓
SecurityContext
       ↓
Authentication
       ├── principal
       ├── credentials
       └── authorities

SecurityContextHolder

  • After successful authentication, Spring stores authenticated-user information in: SecurityContextHolder
  • Spring Security documents SecurityContextHolder as the central place holding the current SecurityContext, which itself contains the current Authentication. ​

Create /api/users/me

22

  • Let's actually prove that.
  • Inside UserController, add:
@GetMapping("/me")
public ResponseEntity<Map<String, Object>> getCurrentUser() {

    Authentication authentication = 
           SecurityContextHolder.getContext().getAuthentication();

    Map<String, Object> response = new LinkedHashMap<>();

    response.put("username",authentication.getName());

    response.put("authorities",authentication.getAuthorities());

    return ResponseEntity.ok(response);
}
  • Add imports :
import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

Test Current User

23

  • Login in Postman with:
rahul@example.com
Rahul@123
  • Call
GET http://localhost:8080/api/users/me
  • Expected approximately:
{
    "username": "rahul@example.com",
    "authorities": [
        {
            "authority": "ROLE_PATIENT"
        }
    ]
}
  • Doctor login would show: ROLE_DOCTOR
Authentication
      ├── Principal
      │      ↓
      │ rahul@example.com
      ├── Credentials
      │      ↓
      │ password-related authentication data
      └── Authorities
             ↓
         ROLE_PATIENT

Understand Authentication

24

The Authentication object represents authenticated-user information and includes the principal, credentials, and granted authorities.

We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below: 

 

Great job!

You have successfully integrated Spring Security into the Medicare backend and implemented Basic Authentication using BCrypt password encryption.

Checkpoint

   Git Push

git push origin branchName

Next-Lab Preparation

Module:

1) Mastering Springboot Security​​