Insights into Company
← Spring Boot

Spring Security

01 What is Spring Security?

Solution and explanation

Solution:

Spring Security is responsible for protecting Spring Boot applications.

Suppose our job portal has these users:

  1. JOB_SEEKER
  2. RECRUITER
  3. HR
  4. ADMIN

A job seeker can:

  • Search jobs
  • View companies
  • Apply for jobs
  • View his own applications

A recruiter can:

  • Create jobs
  • View candidates for his jobs
  • Shortlist candidates

HR can:

  • Manage recruitment
  • Update interview status
  • View recruitment reports

ADMIN can:

  • Manage users
  • Manage companies
  • Manage jobs
  • Manage system configuration

Spring Security handles two different problems.

Authentication:

"Who are you?"

Example:

Sachin logs in using:

email = sachin@example.com password = ********

The application verifies the credentials and creates an authenticated identity.

Authorization:

"What are you allowed to do?"

After Sachin is authenticated, Spring checks his role.

If Sachin is JOB_SEEKER:

GET /api/jobs GET /api/applications POST /api/jobs/10/apply

may be allowed.

But:

DELETE /api/jobs/10

may return 403 Forbidden.

The important difference is:

401 = authentication is missing or invalid.

403 = authentication exists, but the user does not have permission.

Example:

Banking system:

Customer logs in successfully.

Authentication: Customer = ACCOUNT_1001

Authorization:

Customer can:

  • View own balance
  • Transfer money
  • Download statement

Customer cannot:

  • Change another customer's balance
  • Access bank employee administration
  • Approve another customer's loan

Spring Security should therefore be applied at the HTTP and service/method boundaries, not only in the frontend.

Run this example
        @Bean
        SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

            return http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth

                    .requestMatchers(
                        "/api/auth/login",
                        "/api/auth/register",
                        "/api/jobs",
                        "/api/companies/**"
                    ).permitAll()

                    .requestMatchers("/api/admin/**")
                    .hasRole("ADMIN")

                    .requestMatchers("/api/hr/**")
                    .hasAnyRole("HR", "ADMIN")

                    .requestMatchers("/api/recruiter/**")
                    .hasAnyRole("RECRUITER", "HR", "ADMIN")

                    .anyRequest()
                    .authenticated()
                )
                .build();
        }
Hands-on challenge

Build authentication and authorization for JOB_SEEKER, RECRUITER, HR and ADMIN.

Quick check

Which statement is correct?

02 Authentication versus Authorization

Solution and explanation

Solution:

Authentication answers:

"Who is this user?"

Authorization answers:

"What can this user do?"

Consider an Indian banking application.

Customer A logs in.

The authentication process verifies:

  • Customer ID
  • Password
  • OTP or another authentication factor
  • Account/session/token validity

After authentication:

customer_id = 1001

Now authorization begins.

Customer 1001 requests:

GET /api/accounts/1001

The server checks whether account 1001 belongs to the authenticated customer.

If yes:

200 OK

If the customer tries:

GET /api/accounts/2000

the server must not simply trust the account ID received from the URL.

The authenticated identity and ownership must be checked.

The same principle applies to a job portal.

A recruiter may request:

GET /api/jobs/500/applications

The system must verify that recruiter 20 owns job 500 or has explicit permission to access it.

Authentication without authorization would allow a logged-in user to access data belonging to another user.

Therefore:

Authentication = identity.

Authorization = permission.

Run this example
        @GetMapping("/jobs/{jobId}/applications")
        @PreAuthorize("hasAnyRole('RECRUITER','HR','ADMIN')")
        public List<ApplicationDto> applications(
                @PathVariable Long jobId,
                Authentication authentication) {

            return applicationService
                .getApplicationsForAuthorizedRecruiter(
                    jobId,
                    authentication.getName()
                );
        }
Hands-on challenge

Implement ownership validation so one recruiter cannot access another recruiter’s candidate applications.

Quick check

Which statement is correct?

03 How does JWT authentication work?

Solution and explanation

Solution:

JWT authentication is commonly used when a frontend such as React or Angular communicates with a Spring Boot REST API.

Example:

The user submits:

POST /api/auth/login

Request:

email = sachin@example.com password = password123

Spring Security verifies the credentials.

If valid, the backend creates a JWT.

The token contains information such as:

  • subject/user ID
  • roles
  • issuer
  • issued time
  • expiration time

The frontend then sends:

Authorization: Bearer <token>

on subsequent API requests.

Example:

GET /api/applications

The request reaches the Spring Security filter.

The filter:

  1. Reads Authorization header.
  2. Extracts Bearer token.
  3. Verifies the JWT signature.
  4. Checks expiration.
  5. Checks issuer/audience when configured.
  6. Extracts user identity and authorities.
  7. Creates Authentication.
  8. Places it into SecurityContext.
  9. Allows authorization rules to execute.

If the signature is invalid:

401 Unauthorized.

If the token is expired:

401 Unauthorized.

If the token is valid but the role is insufficient:

403 Forbidden.

Never trust a JWT merely because it can be decoded. The payload is readable. The signature must be verified.

Run this example
        @Component
        public class JwtAuthenticationFilter
                extends OncePerRequestFilter {

            private final JwtService jwtService;

            public JwtAuthenticationFilter(JwtService jwtService) {
                this.jwtService = jwtService;
            }

            @Override
            protected void doFilterInternal(
                    HttpServletRequest request,
                    HttpServletResponse response,
                    FilterChain filterChain)
                    throws ServletException, IOException {

                String header = request.getHeader("Authorization");

                if (header == null || !header.startsWith("Bearer ")) {
                    filterChain.doFilter(request, response);
                    return;
                }

                String token = header.substring(7);

                if (jwtService.isValid(token)) {

                    String username =
                        jwtService.extractUsername(token);

                    var authentication =
                        new UsernamePasswordAuthenticationToken(
                            username,
                            null,
                            jwtService.extractAuthorities(token)
                        );

                    SecurityContextHolder
                        .getContext()
                        .setAuthentication(authentication);
                }

                filterChain.doFilter(request, response);
            }
        }
Hands-on challenge

Trace login -> JWT creation -> Authorization header -> JWT validation -> SecurityContext -> controller.

Quick check

Which statement is correct?

04 What is PasswordEncoder and why is BCrypt used?

Solution and explanation

Solution:

Passwords must never be stored directly in a database.

Bad:

users.password = "Sachin@123"

If the database is compromised, the original passwords are immediately exposed.

Instead, the application stores a password hash.

Example:

password entered:

Sachin@123

database:

$2a$10$....

During login:

  1. User enters password.
  2. Spring Security loads stored hash.
  3. PasswordEncoder compares the supplied password with the stored hash.
  4. If it matches, authentication succeeds.
  5. The original password is never recovered from the hash.

BCrypt includes a salt.

Therefore two users having the same password should not necessarily have the same stored hash.

Example:

User A password:

Welcome@123

User B password:

Welcome@123

Their BCrypt hashes can still be different because different salts are used.

Never use MD5 or SHA-1 directly for password storage.

Run this example
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder(12);
        }

        @Service
        public class UserService {

            private final PasswordEncoder passwordEncoder;

            public UserService(PasswordEncoder passwordEncoder) {
                this.passwordEncoder = passwordEncoder;
            }

            public String hashPassword(String password) {

                return passwordEncoder.encode(password);
            }

            public boolean verifyPassword(
                    String rawPassword,
                    String storedHash) {

                return passwordEncoder.matches(
                    rawPassword,
                    storedHash
                );
            }
        }
Hands-on challenge

Create a registration API that stores only a BCrypt hash and a login API that verifies the password using matches().

Quick check

Which statement is correct?

05 What is OAuth2 and OpenID Connect?

Solution and explanation

Solution:

OAuth2 is used when an application wants delegated access to another system.

Example:

A user clicks:

"Login with Google"

The job portal does not need to collect the user's Google password.

The browser is redirected to the identity provider.

The provider authenticates the user.

After successful authentication, the provider redirects back to the application with an authorization result.

OpenID Connect adds an identity layer on top of OAuth2.

For an enterprise application, the same concept can be used with:

  • Google
  • Microsoft
  • GitHub
  • Okta
  • Keycloak
  • enterprise identity providers

Example:

A company has 10,000 employees.

Instead of creating separate passwords in every internal Spring Boot application, employees can authenticate through the organization's identity provider.

The Spring Boot application receives verified identity information and maps it to application roles.

OAuth2 answers delegated authorization.

OpenID Connect provides standardized authentication/identity information.

Run this example
        spring:
          security:
            oauth2:
              client:
                registration:
                  google:
                    client-id: ${GOOGLE_CLIENT_ID}
                    client-secret: ${GOOGLE_CLIENT_SECRET}
                    scope:
                      - openid
                      - profile
                      - email
Hands-on challenge

Implement Google login and map the returned email to an existing JOB_SEEKER or HR account.

Quick check

Which statement is correct?

06 What is CORS and why does it matter in React + Spring Boot?

Solution and explanation

  • Solution:
  • Suppose the frontend runs on:
  • https://jobs.example.com
  • and Spring Boot runs on:
  • https://api.example.com
  • The browser treats these as different origins.
  • The frontend may call:
  • GET https://api.example.com/api/jobs
  • The browser checks whether the API allows the frontend origin.
  • Spring Boot can explicitly allow:
  • https://jobs.example.com
  • but reject:
  • https://unknown-site.com
  • CORS is enforced by browsers.
  • It should not be used as the only API security mechanism.
  • A common mistake is:
  • allowedOrigins("*")
  • for production APIs.
  • Instead, maintain an explicit allow-list.
  • For authenticated browser requests, credentials and CORS configuration must also be handled consistently.
Run this example
        @Configuration
        public class CorsConfig {

            @Bean
            CorsConfigurationSource corsConfigurationSource() {

                CorsConfiguration config =
                    new CorsConfiguration();

                config.setAllowedOrigins(
                    List.of("https://jobs.example.com")
                );

                config.setAllowedMethods(
                    List.of(
                        "GET",
                        "POST",
                        "PUT",
                        "PATCH",
                        "DELETE",
                        "OPTIONS"
                    )
                );

                config.setAllowedHeaders(
                    List.of("Authorization", "Content-Type")
                );

                config.setAllowCredentials(true);

                UrlBasedCorsConfigurationSource source =
                    new UrlBasedCorsConfigurationSource();

                source.registerCorsConfiguration(
                    "/**",
                    config
                );

                return source;
            }
        }
Hands-on challenge

Allow the production React frontend while rejecting requests from an unauthorized browser origin.

Quick check

Which statement is correct?

07 What is role-based authorization?

Solution and explanation

Solution:

Suppose the portal contains these roles:

JOB_SEEKER RECRUITER HR ADMIN

Permissions:

JOB_SEEKER:

  • Search jobs
  • Apply
  • View own applications

RECRUITER:

  • Create jobs
  • View candidates
  • Shortlist candidates

HR:

  • Manage recruitment
  • Schedule interviews
  • View recruitment reports

ADMIN:

  • Manage everything permitted by the application policy

Example:

A recruiter sends:

PUT /api/applications/100/status

with:

status = SHORTLISTED

The backend must check:

  1. Is the recruiter authenticated?
  2. Does the recruiter have recruiter permission?
  3. Does application 100 belong to a job managed by this recruiter?
  4. Is the status transition valid?

Checking only the role is insufficient when resource ownership matters.

The database and service layer must enforce ownership rules.

Run this example
        @PreAuthorize("hasAnyRole('RECRUITER','HR','ADMIN')")
        @PutMapping("/applications/{id}/status")
        public ApplicationDto updateStatus(
                @PathVariable Long id,
                @RequestBody UpdateStatusRequest request,
                Authentication authentication) {

            return applicationService.updateStatus(
                id,
                request,
                authentication.getName()
            );
        }
Hands-on challenge

Prevent recruiter A from updating an application belonging to recruiter B.

Quick check

Which statement is correct?