Skip to content

Latest commit

Β 

History

4 Commits

Folders and files

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

Repository files navigation

πŸ” RBAC API with Keycloak, Spring Boot & PostgreSQL

A complete Role-Based Access Control (RBAC) system implementation using Keycloak for authentication/authorization, Spring Boot for the REST API, and PostgreSQL for data persistence.

πŸ“‹ Table of Contents

πŸ—οΈ Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                 β”‚         β”‚                 β”‚         β”‚                 β”‚
β”‚   Keycloak      │◄─────────  Spring Boot    │◄─────────   PostgreSQL    β”‚
β”‚   (Auth Server) β”‚  JWT    β”‚  (Resource      β”‚  JPA    β”‚   (Database)    β”‚
β”‚                 β”‚  Token  β”‚   Server)       β”‚         β”‚                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The system implements:

  • Authentication: Keycloak issues JWT tokens after user login
  • Authorization: Spring Boot validates JWT tokens and enforces RBAC rules
  • Data Persistence: PostgreSQL stores business resources with organization isolation

🎭 RBAC Model

Roles

Role Description Permissions
ROLE_ADMIN Administrator Full access to all resources in their organization
ROLE_EDITOR Content Editor Create resources, modify their own resources, read all
ROLE_VIEWER Read-only User Read access only

RBAC Rules

  1. Organization Isolation: Users can only access resources within their organization
  2. Role-Based Permissions:
    • VIEWER: GET operations only
    • EDITOR: GET + POST + PUT (own resources only)
    • ADMIN: All operations (GET, POST, PUT, DELETE)
  3. Ownership: Editors can only modify resources they created
  4. Hierarchical: Admin rights supersede all other roles

Keycloak β†’ Spring Security Mapping

Keycloak Realm Roles β†’ JWT Token β†’ Spring Security GrantedAuthority

ROLE_ADMIN (Keycloak)  β†’ realm_access.roles β†’ ROLE_ADMIN (Spring)
ROLE_EDITOR (Keycloak) β†’ realm_access.roles β†’ ROLE_EDITOR (Spring)
ROLE_VIEWER (Keycloak) β†’ realm_access.roles β†’ ROLE_VIEWER (Spring)

The mapping is handled by KeycloakRoleConverter in SecurityConfig.java.

πŸ› οΈ Technology Stack

  • Java: 17
  • Spring Boot: 3.2.2
    • Spring Web
    • Spring Security
    • Spring Data JPA
    • OAuth2 Resource Server
  • Keycloak: 23.0.7
  • PostgreSQL: 15
  • Maven: 3.9+
  • Docker & Docker Compose
  • Swagger/OpenAPI: 3.0

πŸ“¦ Prerequisites

  • Docker & Docker Compose installed
  • Java 17+ (for local development)
  • Maven 3.9+ (for local development)
  • cURL or Postman (for API testing)

πŸš€ Quick Start

1. Clone the repository

git clone <repository-url>
cd keycloak-rbac-project

2. Start all services

docker-compose up --build

This will start:

  • PostgreSQL (port 5432) - Application database
  • Keycloak PostgreSQL (internal) - Keycloak database
  • Keycloak (port 8080) - Authentication server
  • Spring Boot API (port 8081) - REST API

3. Wait for services to be ready

The startup takes approximately 1-2 minutes. You'll know it's ready when you see:

rbac-api        | Started RbacApiApplication in X.XXX seconds
rbac-keycloak   | Keycloak X.X.X started

4. Access the services

5. Test users (pre-configured)

Username Password Role Organization
admin@viasay.io admin123 ROLE_ADMIN org-001
editor@viasay.io editor123 ROLE_EDITOR org-001
viewer@viasay.io viewer123 ROLE_VIEWER org-001

πŸ“š API Documentation

Base URL

http://localhost:8081/api

Endpoints

Method Endpoint Description Required Role
GET /public Public endpoint (no auth) None
GET /me Get current user info Authenticated
GET /resources List all resources VIEWER/EDITOR/ADMIN
GET /resources/{id} Get specific resource VIEWER/EDITOR/ADMIN
POST /resources Create resource EDITOR/ADMIN
PUT /resources/{id} Update resource EDITOR/ADMIN*
DELETE /resources/{id} Delete resource ADMIN

*EDITOR can only update their own resources

Interactive API Documentation

Visit Swagger UI: http://localhost:8081/swagger-ui.html

πŸ§ͺ Testing the API

Step 1: Obtain JWT Token

# Replace {USERNAME} and {PASSWORD} with one of the test users
curl -X POST "http://localhost:8080/realms/rbac-realm/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id=rbac-client" \
  -d "client_secret=rbac-client-secret-12345" \
  -d "grant_type=password" \
  -d "username=admin@viasay.io" \
  -d "password=admin123"

Response:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_in": 3600,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer"
}

Save the access_token for subsequent requests.

Step 2: Test Public Endpoint (No Auth)

curl -X GET "http://localhost:8081/api/public"

Expected Response:

{
  "message": "This is a public endpoint accessible without authentication",
  "timestamp": "1707123456789"
}

Step 3: Test User Info Endpoint

# Replace {TOKEN} with your access_token
curl -X GET "http://localhost:8081/api/me" \
  -H "Authorization: Bearer {TOKEN}"

Expected Response:

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "username": "admin@viasay.io",
  "email": "admin@viasay.io",
  "firstName": "Admin",
  "lastName": "User",
  "organizationId": "org-001",
  "roles": ["ROLE_ADMIN"]
}

Step 4: Create a Resource (EDITOR or ADMIN)

curl -X POST "http://localhost:8081/api/resources" \
  -H "Authorization: Bearer {TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My First Project",
    "content": "This is a test project content"
  }'

Expected Response:

{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "name": "My First Project",
  "content": "This is a test project content",
  "ownerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "organizationId": "org-001",
  "createdAt": "2024-02-05T10:30:00",
  "updatedAt": "2024-02-05T10:30:00"
}

Step 5: List All Resources (Any authenticated user)

curl -X GET "http://localhost:8081/api/resources" \
  -H "Authorization: Bearer {TOKEN}"

Step 6: Update a Resource (Owner or ADMIN)

curl -X PUT "http://localhost:8081/api/resources/{RESOURCE_ID}" \
  -H "Authorization: Bearer {TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Updated Project Name",
    "content": "Updated content"
  }'

Step 7: Delete a Resource (ADMIN only)

curl -X DELETE "http://localhost:8081/api/resources/{RESOURCE_ID}" \
  -H "Authorization: Bearer {TOKEN}"

Expected Response: 204 No Content

Testing RBAC Restrictions

Test 1: VIEWER trying to create (should fail)

# Get token for viewer
TOKEN=$(curl -s -X POST "http://localhost:8080/realms/rbac-realm/protocol/openid-connect/token" \
  -d "client_id=rbac-client" \
  -d "client_secret=rbac-client-secret-12345" \
  -d "grant_type=password" \
  -d "username=viewer@viasay.io" \
  -d "password=viewer123" | jq -r '.access_token')

# Try to create (should return 403 Forbidden)
curl -X POST "http://localhost:8081/api/resources" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Test", "content": "Test"}'

Test 2: EDITOR trying to update another user's resource (should fail)

# 1. Admin creates a resource
# 2. Editor tries to update it (should return 403 Forbidden)

Test 3: EDITOR trying to delete (should fail)

# Get token for editor
TOKEN=$(curl -s -X POST "http://localhost:8080/realms/rbac-realm/protocol/openid-connect/token" \
  -d "client_id=rbac-client" \
  -d "client_secret=rbac-client-secret-12345" \
  -d "grant_type=password" \
  -d "username=editor@viasay.io" \
  -d "password=editor123" | jq -r '.access_token')

# Try to delete (should return 403 Forbidden)
curl -X DELETE "http://localhost:8081/api/resources/{RESOURCE_ID}" \
  -H "Authorization: Bearer $TOKEN"

πŸ”’ RBAC Rules Implementation

1. Controller Level (@PreAuthorize)

Used for coarse-grained access control:

@PreAuthorize("hasRole('ROLE_ADMIN')")
public void deleteResource(UUID id) { ... }

2. Service Level (Programmatic Checks)

Used for fine-grained business logic:

// EDITOR can only update their own resources
if (securityUtils.isEditor() && !resource.getOwnerId().equals(userId)) {
    throw new ForbiddenException("You can only update your own resources");
}

3. Repository Level (Data Filtering)

Used to enforce organization isolation:

List<Resource> findByOrganizationId(String organizationId);
Optional<Resource> findByIdAndOrganizationId(UUID id, String organizationId);

Security Flow

1. Request arrives with JWT token
   ↓
2. Spring Security validates JWT (signature, expiration, issuer)
   ↓
3. KeycloakRoleConverter extracts roles from JWT
   ↓
4. @PreAuthorize checks role permissions
   ↓
5. SecurityUtils provides user context (organizationId, userId)
   ↓
6. Service layer enforces business rules
   ↓
7. Repository layer filters data by organization
   ↓
8. Response sent to client

πŸ›‘οΈ Security Configuration

JWT Token Validation

The API validates:

  • βœ… Signature: Using Keycloak's public key
  • βœ… Expiration: Token must not be expired
  • βœ… Issuer: Must match configured issuer URI
  • βœ… Claims: Extracts roles and organizationId

Error Responses

Status Code Description
401 Unauthorized Missing or invalid JWT token
403 Forbidden Valid token but insufficient permissions
404 Not Found Resource doesn't exist or wrong organization

πŸ“ Project Structure

keycloak-rbac-project/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ main/
β”‚   β”‚   β”œβ”€β”€ java/com/viasay/rbac/
β”‚   β”‚   β”‚   β”œβ”€β”€ RbacApiApplication.java
β”‚   β”‚   β”‚   β”œβ”€β”€ config/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ SecurityConfig.java
β”‚   β”‚   β”‚   β”‚   └── OpenApiConfig.java
β”‚   β”‚   β”‚   β”œβ”€β”€ controller/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ PublicController.java
β”‚   β”‚   β”‚   β”‚   └── ResourceController.java
β”‚   β”‚   β”‚   β”œβ”€β”€ service/
β”‚   β”‚   β”‚   β”‚   └── ResourceService.java
β”‚   β”‚   β”‚   β”‚   └── ResourceServiceImpl.java
β”‚   β”‚   β”‚   β”œβ”€β”€ repository/
β”‚   β”‚   β”‚   β”‚   └── ResourceRepository.java
β”‚   β”‚   β”‚   β”œβ”€β”€ entity/
β”‚   β”‚   β”‚   β”‚   └── Resource.java
β”‚   β”‚   β”‚   β”œβ”€β”€ dto/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ResourceDto.java
β”‚   β”‚   β”‚   β”‚   └── UserInfoDto.java
β”‚   β”‚   β”‚   β”œβ”€β”€ exception/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ GlobalExceptionHandler.java
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ResourceNotFoundException.java
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ForbiddenException.java
β”‚   β”‚   β”‚   β”‚   └── ErrorResponse.java
β”‚   β”‚   β”‚   └── util/
β”‚   β”‚   β”‚       └── SecurityUtils.java
β”‚   β”‚   └── resources/
β”‚   β”‚       └── application.yml
β”‚   └── test/
β”‚       └── java/com/viasay/rbac/
β”‚           └── (test files)
β”œβ”€β”€ keycloak/
β”‚   └── realm-export.json
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ pom.xml
└── README.md

🌍 Environment Variables

Spring Boot Application

Variable Default Description
SPRING_DATASOURCE_URL jdbc:postgresql://localhost:5432/rbac_db PostgreSQL connection URL
SPRING_DATASOURCE_USERNAME rbac_user Database username
SPRING_DATASOURCE_PASSWORD rbac_password Database password
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI http://localhost:8080/realms/rbac-realm Keycloak issuer URI
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI http://localhost:8080/realms/rbac-realm/protocol/openid-connect/certs JWK Set URI

Keycloak

Variable Default Description
KEYCLOAK_ADMIN admin Keycloak admin username
KEYCLOAK_ADMIN_PASSWORD admin Keycloak admin password
KC_DB postgres Database type
KC_DB_URL jdbc:postgresql://keycloak-postgres:5432/keycloak Keycloak database URL

πŸ”§ Troubleshooting

Issue: Services not starting

Solution: Ensure Docker has sufficient resources (4GB RAM minimum)

docker-compose down -v
docker-compose up --build

Issue: Keycloak realm not imported

Solution: Check that keycloak/realm-export.json exists and restart Keycloak

docker-compose restart keycloak

Issue: 401 Unauthorized errors

Causes:

  1. Token expired (tokens expire after 1 hour)
  2. Invalid token format
  3. Keycloak not accessible from Spring Boot

Solution:

  • Obtain a fresh token
  • Check token format: Authorization: Bearer {token}
  • Verify Keycloak is running: curl http://localhost:8080/health

Issue: 403 Forbidden errors

Causes:

  1. User doesn't have required role
  2. Trying to access another organization's resources
  3. EDITOR trying to modify someone else's resource

Solution: Verify user roles and resource ownership

Issue: Database connection errors

Solution: Wait for PostgreSQL to be fully ready

docker-compose logs postgres

🧹 Cleanup

Stop all services:

docker-compose down

Remove all data (volumes):

docker-compose down -v

πŸ“Š Database Schema

Resources Table

CREATE TABLE resources (
    id UUID PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    content TEXT,
    owner_id VARCHAR(255) NOT NULL,
    organization_id VARCHAR(255) NOT NULL,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP
);

CREATE INDEX idx_resources_organization_id ON resources(organization_id);
CREATE INDEX idx_resources_owner_id ON resources(owner_id);

🎯 Key Implementation Choices

  1. @PreAuthorize vs Custom Filters: Used @PreAuthorize for clarity and declarative security
  2. Service Layer RBAC: Business rules enforced in service layer for testability
  3. Organization Isolation: Repository layer ensures data isolation
  4. SecurityUtils: Centralized helper for extracting JWT claims
  5. Global Exception Handler: Consistent error responses across the API

πŸ“ Additional Notes

  • Tokens expire after 900 seconds (15 minutes)
  • Refresh tokens can be used to obtain new access tokens
  • All timestamps are in UTC
  • UUIDs are used for all entity IDs
  • Soft delete is not implemented (hard delete)
  • Audit logging can be added via JPA auditing

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages