Skip to content

About

Spring Security setting up role based access and database authentication

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

8 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Spring Security JWT Authentication

A Spring Boot REST API secured with JWT authentication, role-based access control (RBAC), and JDBC-backed user storage using Spring Security's built-in JdbcUserDetailsManager.

Users log in with a username/password, receive a signed JWT, and use that token on subsequent requests to access role-protected endpoints.

🎯 Features

  • JWT Authentication : stateless, token-based login via /Signin
  • Role-Based Access Control : @PreAuthorize guards on USER and ADMIN endpoints
  • JDBC User Storage : accounts persisted through Spring Security's standard users / authorities schema
  • Custom JWT Filter : AuthTokenFilter validates the token on every request before it reaches a controller
  • Custom 401 Handling : AuthEntryPointJwt returns a clean JSON error instead of Spring's default redirect
  • H2 In-Memory Database : zero setup for local development, with the H2 console enabled
  • BCrypt Password Hashing : no plain-text passwords, ever

πŸ›  Tech Stack

  • Java 17
  • Spring Boot (Spring Security, Spring Web)
  • JJWT : JWT creation and validation
  • H2 Database : in-memory, dev/test only
  • Maven

πŸ“‹ Prerequisites

  • Java 17+
  • Maven 3.6+
  • Git

πŸš€ Getting Started

1. Clone the repository

git clone https://github.com/Ayshasharaf/Spring-Security-JWT-Authentication.git
cd Spring-Security-JWT-Authentication

2. Build

./mvnw clean install

3. Run

./mvnw spring-boot:run

The app starts on http://localhost:8080.

πŸ“ Project Structure

src/main/java/com/learn/springsecurity/
β”œβ”€β”€ SecurityConfig.java          # Filter chain, password encoder, AuthenticationManager, seeds test users
β”œβ”€β”€ GreetingsController.java     # /hello, /user, /admin, /Signin endpoints
└── jwt/
    β”œβ”€β”€ JwtUtils.java            # Generates/validates JWTs, extracts claims (referenced by filter & controller)
    β”œβ”€β”€ AuthTokenFilter.java     # Runs once per request : validates JWT, sets SecurityContext
    β”œβ”€β”€ AuthEntryPointJwt.java   # Returns JSON 401 on failed authentication
    β”œβ”€β”€ LoginRequest.java        # DTO: { "username", "password" }
    └── LoginResponse.java       # DTO: username, roles, JWT (referenced by controller)

src/main/resources/
β”œβ”€β”€ application.properties
└── schema.sql                  # Spring Security's standard users / authorities tables

πŸ” How It Works

  1. On startup, SecurityConfig seeds two accounts directly into the database via JdbcUserDetailsManager:
    • user1 / Aysha@test β†’ role USER
    • admin / admin@test β†’ role ADMIN
  2. Client sends credentials to POST /Signin.
  3. AuthenticationManager verifies the credentials against the DB (BCrypt-hashed).
  4. On success, JwtUtils generates a signed JWT and it's returned in the response body.
  5. For every later request, the client sends Authorization: Bearer <token>.
  6. AuthTokenFilter intercepts the request, validates the token, loads the user via UserDetailsService, and populates Spring Security's SecurityContext.
  7. @PreAuthorize checks the user's roles before allowing access to /user or /admin.
  8. If the JWT is missing/invalid, AuthEntryPointJwt returns a JSON 401 Unauthorized.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”                                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Client β”‚                                   β”‚ Server β”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜                                   β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
    │── POST /Signin {username, password} ──────►│
    β”‚                                             β”‚  validate creds (DB + BCrypt)
    β”‚                                             β”‚  generate JWT
    │◄──────── { username, roles, token } ────────│
    β”‚                                             β”‚
    │── GET /admin  (Authorization: Bearer <jwt>)β–Ίβ”‚
    β”‚                                             β”‚  AuthTokenFilter validates token
    β”‚                                             β”‚  @PreAuthorize checks role
    │◄──────────── "Hello Admin:)" ────────────────│

πŸ“š API Endpoints

Public

Method Endpoint Description
GET /hello Unprotected health-check endpoint
POST /Signin Login, returns a JWT

Login request

POST /Signin
Content-Type: application/json

{
  "username": "admin",
  "password": "admin@test"
}

Login response

{
  "username": "admin",
  "roles": ["ROLE_ADMIN"],
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Login failure (401 / 404-style error body)

{
  "message": "Invalid username or password",
  "status": false
}

Protected

Method Endpoint Required Role Description
GET /user USER Returns "Hello User:)"
GET /admin ADMIN Returns "Hello Admin:)"

Send the token from /Signin as a Bearer token:

GET /admin
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Unauthenticated / invalid token response

{
  "status": 401,
  "error": "Unauthorized",
  "message": "Full authentication is required to access this resource",
  "path": "/admin"
}

πŸ§ͺ Tested with Postman

All endpoints above were manually verified using Postman.

Flow used for testing:

  1. POST http://localhost:8080/Signin with the JSON body shown above (admin / admin@test or user1 / Aysha@test) β†’ copy the token from the response.
  2. For GET http://localhost:8080/user or GET http://localhost:8080/admin, open the Authorization tab in Postman, set type to Bearer Token, and paste the copied token.
  3. Confirm:
    • admin token β†’ /admin returns 200 Hello Admin:), /user returns 403 Forbidden
    • user1 token β†’ /user returns 200 Hello User:), /admin returns 403 Forbidden
    • No token / expired / tampered token β†’ 401 Unauthorized with the JSON error body from AuthEntryPointJwt
    • Wrong credentials on /Signin β†’ 404 with { "message": "Invalid username or password", "status": false }

πŸ—„οΈ Database

Schema (schema.sql)

Spring Security's JdbcUserDetailsManager expects these exact tables:

create table users (
    username varchar_ignorecase(50) not null primary key,
    password varchar_ignorecase(500) not null,
    enabled boolean not null
);

create table authorities (
    username varchar_ignorecase(50) not null,
    authority varchar_ignorecase(50) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);

create unique index ix_auth_username on authorities (username, authority);

Seeded test accounts

Username Password Role
user1 Aysha@test USER
admin admin@test ADMIN

H2 Console

  • URL: http://localhost:8080/h2-console
  • JDBC URL: jdbc:h2:mem:test
  • Username: sa
  • Password: (leave empty)

Verifying seeded data in H2 Console

Handy for confirming the SecurityConfig seeding and schema.sql actually ran before testing in Postman:

  1. Start the app, then open http://localhost:8080/h2-console in a browser.
  2. On the login screen, make sure JDBC URL matches jdbc:h2:mem:test exactly (must match spring.datasource.url), username sa, password blank β†’ Connect.
  3. Run a quick check query:
   SELECT * FROM users;
   SELECT * FROM authorities;

You should see user1 and admin in users, each hashed with BCrypt, and matching ROLE_USER / ROLE_ADMIN rows in authorities. 4. If the tables are missing or empty, check that spring.sql.init.mode=always and spring.jpa.defer-datasource-initialization=true are both set , otherwise schema.sql may run before Hibernate creates the schema, or not run at all.

⚠️ The H2 console is convenient for local dev but should never be exposed in a production profile , disable spring.h2.console.enabled outside of dev/test.

πŸ“ Configuration (application.properties)

spring.application.name=SpringSecurity

# Run schema.sql after JPA initializes so the tables exist first
spring.sql.init.mode=always
spring.jpa.defer-datasource-initialization=true

# H2
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:test

# JWT
spring.app.jwtSecret=AyshasSecretKey1092767788786867hgcgfcgcghvhjbilbmkj
spring.app.jwtExpirationMs=3000000000000000

# Logging (verbose for learning/debugging security flow)
logging.level.org.springframework=DEBUG
logging.level.org.springframework.security=DEBUG
logging.level.org.hibernate.SQL=DEBUG

⚠️ Before deploying anywhere real:

  • Move spring.app.jwtSecret into an environment variable : never commit real secrets.
  • spring.app.jwtExpirationMs above is absurdly large (far beyond a valid long in practice); set a sane value such as 3600000 (1 hour).
  • Turn logging back down from DEBUG in production.
  • Replace H2 with a persistent database (PostgreSQL, MySQL, etc.) : H2 here resets on every restart.

πŸ”’ Security Notes

  • Passwords are hashed with BCrypt before being stored : never compared or stored in plain text.
  • The app is fully stateless (SessionCreationPolicy.STATELESS) : no server-side session, the JWT carries identity on every request.
  • CSRF protection is disabled, which is standard practice for stateless, token-based JWT APIs (there's no session cookie to exploit).
  • Framing is allowed same-origin only, just enough to let the H2 console render in an iframe.
  • Failed authentication never leaks whether it was the username or the password that was wrong : both return a generic "Invalid username or password".

πŸ”„ Possible Next Steps

  • Refresh token support
  • Move secrets/config to environment variables
  • Swap H2 for a persistent database
  • Add user registration endpoint
  • Rate limiting on /Signin
  • Swagger/OpenAPI docs

πŸ‘€ Author

Aysha Sharaf

πŸ“„ License

MIT

About

Spring Security setting up role based access and database authentication

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages