google-site-verification: google61fe8ba583a51912.html
PostgREST Security in Java: Complete Implementation Guide

PostgREST is a powerful tool that turns PostgreSQL databases into RESTful APIs. This guide provides comprehensive Java-based security implementations for securing PostgREST APIs with authentication, authorization, row-level security, and API gateway integration.


PostgREST Security Architecture

Security Layers:

  1. API Gateway Security - Rate limiting, IP whitelisting, SSL termination
  2. Authentication - JWT tokens, API keys, OAuth2
  3. Authorization - Row Level Security (RLS), role-based access
  4. Database Security - RLS policies, schema permissions, function security

Dependencies and Setup

Maven Dependencies
<properties> <spring-boot.version>3.1.0</spring-boot.version> <jjwt.version>0.11.5</jjwt.version> <postgresql.version>42.6.0</postgresql.version> </properties> <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Database --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>${postgresql.version}</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <!-- Utilities --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> </dependencies>
Application Configuration
# application.yml server: port: 8080 spring: datasource: url: jdbc:postgresql://localhost:5432/postgrest_secure username: postgrest_api password: ${DB_PASSWORD:securepassword} driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: validate show-sql: false properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect app: security: # JWT Configuration jwt: secret: ${JWT_SECRET:your-super-secure-jwt-secret-key-minimum-32-chars} expiration: 3600 # 1 hour issuer: postgrest-security # API Gateway Configuration gateway: rate-limit: requests-per-minute: 100 burst-capacity: 20 ip-whitelist: enabled: true allowed-ips: "192.168.1.0/24,10.0.0.0/8" # PostgREST Configuration postgrest: base-url: "http://localhost:3000" schema: "api" anon-role: "web_anon" auth-role: "web_user" # Row Level Security rls: enabled: true auto-create-policies: true logging: level: com.example.postgrestsecurity: DEBUG org.springframework.security: INFO

Database Security Models

1. Database Schema and RLS Setup
-- Database setup for PostgREST with RLS CREATE DATABASE postgrest_secure; -- Create roles for PostgREST CREATE ROLE web_anon NOLOGIN; CREATE ROLE web_user NOLOGIN; CREATE ROLE authenticator NOINHERIT LOGIN PASSWORD 'securepassword'; GRANT web_anon TO authenticator; GRANT web_user TO authenticator; -- Create API schema CREATE SCHEMA api; CREATE SCHEMA auth; CREATE SCHEMA private; -- Set search path ALTER DATABASE postgrest_secure SET search_path TO api, auth, private; -- Grant usage to roles GRANT USAGE ON SCHEMA api TO web_anon, web_user; GRANT USAGE ON SCHEMA auth TO web_anon, web_user; GRANT USAGE ON SCHEMA private TO web_user;
2. User and Role Management
// User.java - Application user entity package com.example.postgrestsecurity.entity; import lombok.Data; import lombok.EqualsAndHashCode; import jakarta.persistence.*; import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "users", schema = "auth") @Data @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @EqualsAndHashCode.Include @Column(name = "username", unique = true, nullable = false) private String username; @Column(name = "email", unique = true, nullable = false) private String email; @Column(name = "password_hash") private String passwordHash; @Column(name = "is_active", nullable = false) private Boolean isActive = true; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @Column(name = "last_login") private LocalDateTime lastLogin; @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "user_roles", schema = "auth", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id") ) private Set<Role> roles = new HashSet<>(); @ElementCollection @CollectionTable(name = "user_tenants", schema = "auth", joinColumns = @JoinColumn(name = "user_id")) @Column(name = "tenant_id") private Set<String> tenantIds = new HashSet<>(); @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } } // Role.java - Role entity package com.example.postgrestsecurity.entity; import lombok.Data; import jakarta.persistence.*; import java.time.LocalDateTime; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "roles", schema = "auth") @Data public class Role { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", unique = true, nullable = false) private String name; @Column(name = "description") private String description; @Column(name = "is_system", nullable = false) private Boolean isSystem = false; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "role_permissions", schema = "auth", joinColumns = @JoinColumn(name = "role_id"), inverseJoinColumns = @JoinColumn(name = "permission_id") ) private Set<Permission> permissions = new HashSet<>(); @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); } } // Permission.java - Permission entity package com.example.postgrestsecurity.entity; import lombok.Data; import jakarta.persistence.*; import java.time.LocalDateTime; @Entity @Table(name = "permissions", schema = "auth") @Data public class Permission { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "name", unique = true, nullable = false) private String name; @Column(name = "description") private String description; @Column(name = "resource") private String resource; @Column(name = "action") private String action; // CREATE, READ, UPDATE, DELETE @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); } }
3. JWT Token Models
// JwtToken.java package com.example.postgrestsecurity.model; import lombok.Data; import lombok.Builder; import java.time.LocalDateTime; import java.util.Map; import java.util.Set; @Data @Builder public class JwtToken { private String token; private String type; private LocalDateTime issuedAt; private LocalDateTime expiresAt; private Map<String, Object> claims; public boolean isValid() { return expiresAt != null && expiresAt.isAfter(LocalDateTime.now()); } } // JwtClaims.java package com.example.postgrestsecurity.model; import lombok.Data; import lombok.Builder; import java.util.Set; @Data @Builder public class JwtClaims { private String subject; private String username; private String email; private Set<String> roles; private Set<String> permissions; private Set<String> tenantIds; private String issuer; private LocalDateTime issuedAt; private LocalDateTime expiresAt; // PostgREST specific claims private String role; private Map<String, String> databaseClaims; public static class DatabaseClaims { public static final String USER_ID = "user_id"; public static final String TENANT_ID = "tenant_id"; public static final String ROLE = "role"; } } // AuthRequest.java package com.example.postgrestsecurity.model; import lombok.Data; import jakarta.validation.constraints.NotBlank; @Data public class AuthRequest { @NotBlank(message = "Username is required") private String username; @NotBlank(message = "Password is required") private String password; private String tenantId; } // AuthResponse.java package com.example.postgrestsecurity.model; import lombok.Data; import lombok.Builder; @Data @Builder public class AuthResponse { private boolean success; private String message; private String accessToken; private String refreshToken; private Long expiresIn; private String tokenType; private UserInfo user; @Data @Builder public static class UserInfo { private Long id; private String username; private String email; private Set<String> roles; private Set<String> permissions; private Set<String> tenantIds; } }

JWT Token Service

// JwtTokenService.java package com.example.postgrestsecurity.service; import com.example.postgrestsecurity.entity.User; import com.example.postgrestsecurity.model.JwtClaims; import com.example.postgrestsecurity.model.JwtToken; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import javax.crypto.SecretKey; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; @Slf4j @Service public class JwtTokenService { private final SecretKey secretKey; private final long expiration; private final String issuer; public JwtTokenService( @Value("${app.security.jwt.secret}") String secret, @Value("${app.security.jwt.expiration}") long expiration, @Value("${app.security.jwt.issuer}") String issuer) { this.secretKey = Keys.hmacShaKeyFor(secret.getBytes()); this.expiration = expiration; this.issuer = issuer; } /** * Generate JWT token for user with PostgREST claims */ public JwtToken generateToken(User user) { LocalDateTime now = LocalDateTime.now(); LocalDateTime expiresAt = now.plusSeconds(expiration); // Build claims for PostgREST Map<String, Object> claims = buildPostgRESTClaims(user); String token = Jwts.builder() .setIssuer(issuer) .setSubject(user.getId().toString()) .setAudience("postgrest-api") .setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant())) .setExpiration(Date.from(expiresAt.atZone(ZoneId.systemDefault()).toInstant())) .addClaims(claims) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); return JwtToken.builder() .token(token) .type("Bearer") .issuedAt(now) .expiresAt(expiresAt) .claims(claims) .build(); } /** * Build PostgREST specific claims */ private Map<String, Object> buildPostgRESTClaims(User user) { Map<String, Object> claims = new HashMap<>(); // Standard claims claims.put("username", user.getUsername()); claims.put("email", user.getEmail()); // Role and permission claims claims.put("roles", user.getRoles().stream() .map(role -> role.getName()) .collect(Collectors.toSet())); claims.put("permissions", user.getRoles().stream() .flatMap(role -> role.getPermissions().stream()) .map(permission -> permission.getName()) .collect(Collectors.toSet())); // Tenant claims claims.put("tenant_ids", user.getTenantIds()); // PostgREST specific claims for RLS claims.put("role", "web_user"); // PostgREST role // Database claims for RLS policies Map<String, String> databaseClaims = new HashMap<>(); databaseClaims.put("user_id", user.getId().toString()); if (!user.getTenantIds().isEmpty()) { databaseClaims.put("tenant_id", user.getTenantIds().iterator().next()); } databaseClaims.put("role", "web_user"); claims.put("app_metadata", databaseClaims); return claims; } /** * Validate and parse JWT token */ public JwtClaims validateToken(String token) { try { Claims claims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); LocalDateTime expiresAt = claims.getExpiration().toInstant() .atZone(ZoneId.systemDefault()) .toLocalDateTime(); // Extract PostgREST claims return JwtClaims.builder() .subject(claims.getSubject()) .username(claims.get("username", String.class)) .email(claims.get("email", String.class)) .roles(claims.get("roles", java.util.Set.class)) .permissions(claims.get("permissions", java.util.Set.class)) .tenantIds(claims.get("tenant_ids", java.util.Set.class)) .issuer(claims.getIssuer()) .issuedAt(claims.getIssuedAt().toInstant() .atZone(ZoneId.systemDefault()) .toLocalDateTime()) .expiresAt(expiresAt) .role(claims.get("role", String.class)) .databaseClaims(claims.get("app_metadata", java.util.Map.class)) .build(); } catch (ExpiredJwtException e) { log.warn("JWT token expired: {}", e.getMessage()); throw new SecurityException("Token expired", e); } catch (MalformedJwtException e) { log.warn("Invalid JWT token: {}", e.getMessage()); throw new SecurityException("Invalid token", e); } catch (JwtException e) { log.error("JWT validation error: {}", e.getMessage()); throw new SecurityException("Token validation failed", e); } } /** * Generate PostgREST compatible JWT token */ public String generatePostgRESTToken(User user) { Map<String, Object> claims = buildPostgRESTClaims(user); return Jwts.builder() .setIssuer(issuer) .setSubject(user.getId().toString()) .setExpiration(Date.from(LocalDateTime.now().plusSeconds(expiration) .atZone(ZoneId.systemDefault()).toInstant())) .addClaims(claims) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); } /** * Refresh token */ public JwtToken refreshToken(String token) { JwtClaims claims = validateToken(token); // In a real implementation, you would fetch the user from database // and generate a new token throw new UnsupportedOperationException("Token refresh not implemented"); } }

Authentication Service

// AuthenticationService.java package com.example.postgrestsecurity.service; import com.example.postgrestsecurity.entity.User; import com.example.postgrestsecurity.model.AuthRequest; import com.example.postgrestsecurity.model.AuthResponse; import com.example.postgrestsecurity.repository.UserRepository; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.Optional; @Slf4j @Service @Transactional public class AuthenticationService { private final UserRepository userRepository; private final JwtTokenService jwtTokenService; private final PasswordEncoder passwordEncoder; public AuthenticationService(UserRepository userRepository, JwtTokenService jwtTokenService, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.jwtTokenService = jwtTokenService; this.passwordEncoder = passwordEncoder; } /** * Authenticate user and generate JWT token */ public AuthResponse authenticate(AuthRequest authRequest) { try { Optional<User> userOpt = userRepository.findByUsername(authRequest.getUsername()); if (userOpt.isEmpty()) { log.warn("Authentication failed: user not found - {}", authRequest.getUsername()); return AuthResponse.builder() .success(false) .message("Invalid credentials") .build(); } User user = userOpt.get(); // Check if user is active if (!user.getIsActive()) { log.warn("Authentication failed: user inactive - {}", authRequest.getUsername()); return AuthResponse.builder() .success(false) .message("Account is disabled") .build(); } // Verify password if (!passwordEncoder.matches(authRequest.getPassword(), user.getPasswordHash())) { log.warn("Authentication failed: invalid password - {}", authRequest.getUsername()); return AuthResponse.builder() .success(false) .message("Invalid credentials") .build(); } // Check tenant access if tenantId is provided if (authRequest.getTenantId() != null && !user.getTenantIds().contains(authRequest.getTenantId())) { log.warn("Authentication failed: tenant access denied - user: {}, tenant: {}", authRequest.getUsername(), authRequest.getTenantId()); return AuthResponse.builder() .success(false) .message("Access denied to tenant") .build(); } // Update last login user.setLastLogin(LocalDateTime.now()); userRepository.save(user); // Generate tokens String accessToken = jwtTokenService.generatePostgRESTToken(user); log.info("Authentication successful for user: {}", authRequest.getUsername()); return AuthResponse.builder() .success(true) .message("Authentication successful") .accessToken(accessToken) .tokenType("Bearer") .expiresIn(3600L) .user(buildUserInfo(user)) .build(); } catch (Exception e) { log.error("Authentication error for user: {}", authRequest.getUsername(), e); return AuthResponse.builder() .success(false) .message("Authentication failed: " + e.getMessage()) .build(); } } /** * Validate JWT token */ public boolean validateToken(String token) { try { jwtTokenService.validateToken(token); return true; } catch (Exception e) { log.debug("Token validation failed: {}", e.getMessage()); return false; } } /** * Get user from JWT token */ public Optional<User> getUserFromToken(String token) { try { com.example.postgrestsecurity.model.JwtClaims claims = jwtTokenService.validateToken(token); return userRepository.findById(Long.parseLong(claims.getSubject())); } catch (Exception e) { log.debug("Failed to get user from token: {}", e.getMessage()); return Optional.empty(); } } private AuthResponse.UserInfo buildUserInfo(User user) { return AuthResponse.UserInfo.builder() .id(user.getId()) .username(user.getUsername()) .email(user.getEmail()) .roles(user.getRoles().stream() .map(role -> role.getName()) .collect(java.util.stream.Collectors.toSet())) .permissions(user.getRoles().stream() .flatMap(role -> role.getPermissions().stream()) .map(permission -> permission.getName()) .collect(java.util.stream.Collectors.toSet())) .tenantIds(user.getTenantIds()) .build(); } }

Row Level Security (RLS) Service

// RowLevelSecurityService.java package com.example.postgrestsecurity.service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.util.List; import java.util.Map; @Slf4j @Service public class RowLevelSecurityService { private final JdbcTemplate jdbcTemplate; private final boolean rlsEnabled; public RowLevelSecurityService(JdbcTemplate jdbcTemplate, @Value("${app.security.rls.enabled:true}") boolean rlsEnabled) { this.jdbcTemplate = jdbcTemplate; this.rlsEnabled = rlsEnabled; } @PostConstruct public void initializeRLS() { if (!rlsEnabled) { log.info("Row Level Security is disabled"); return; } try { enableRLS(); createRLSPolicies(); log.info("Row Level Security initialized successfully"); } catch (Exception e) { log.error("Failed to initialize Row Level Security", e); } } /** * Enable RLS on tables */ public void enableRLS() { // Enable RLS on API schema tables String[] tables = {"users", "products", "orders", "tenants"}; for (String table : tables) { try { jdbcTemplate.execute("ALTER TABLE api." + table + " ENABLE ROW LEVEL SECURITY"); log.debug("Enabled RLS on table: {}", table); } catch (Exception e) { log.warn("Failed to enable RLS on table {}: {}", table, e.getMessage()); } } } /** * Create RLS policies */ public void createRLSPolicies() { createUserPolicies(); createProductPolicies(); createOrderPolicies(); createTenantPolicies(); } /** * RLS policies for users table */ private void createUserPolicies() { try { // Drop existing policies jdbcTemplate.execute("DROP POLICY IF EXISTS users_select_policy ON api.users"); jdbcTemplate.execute("DROP POLICY IF EXISTS users_insert_policy ON api.users"); jdbcTemplate.execute("DROP POLICY IF EXISTS users_update_policy ON api.users"); jdbcTemplate.execute("DROP POLICY IF EXISTS users_delete_policy ON api.users"); // Users can read their own data jdbcTemplate.execute( "CREATE POLICY users_select_policy ON api.users FOR SELECT " + "USING (id = current_setting('request.jwt.claims', true)::json->>'user_id'::bigint " + "OR 'admin' = ANY(current_setting('request.jwt.claims', true)::json->'roles'))" ); // Users can update their own data jdbcTemplate.execute( "CREATE POLICY users_update_policy ON api.users FOR UPDATE " + "USING (id = current_setting('request.jwt.claims', true)::json->>'user_id'::bigint)" ); log.debug("Created user RLS policies"); } catch (Exception e) { log.error("Failed to create user RLS policies: {}", e.getMessage()); } } /** * RLS policies for products table (multi-tenant) */ private void createProductPolicies() { try { jdbcTemplate.execute("DROP POLICY IF EXISTS products_select_policy ON api.products"); jdbcTemplate.execute("DROP POLICY IF EXISTS products_insert_policy ON api.products"); jdbcTemplate.execute("DROP POLICY IF EXISTS products_update_policy ON api.products"); jdbcTemplate.execute("DROP POLICY IF EXISTS products_delete_policy ON api.products"); // Users can read products from their tenants jdbcTemplate.execute( "CREATE POLICY products_select_policy ON api.products FOR SELECT " + "USING (tenant_id = ANY(string_to_array(current_setting('request.jwt.claims', true)::json->'app_metadata'->>'tenant_ids', ',')::text[]))" ); // Users can insert products for their tenants jdbcTemplate.execute( "CREATE POLICY products_insert_policy ON api.products FOR INSERT " + "WITH CHECK (tenant_id = ANY(string_to_array(current_setting('request.jwt.claims', true)::json->'app_metadata'->>'tenant_ids', ',')::text[]))" ); // Users can update products from their tenants jdbcTemplate.execute( "CREATE POLICY products_update_policy ON api.products FOR UPDATE " + "USING (tenant_id = ANY(string_to_array(current_setting('request.jwt.claims', true)::json->'app_metadata'->>'tenant_ids', ',')::text[]))" ); // Users can delete products from their tenants jdbcTemplate.execute( "CREATE POLICY products_delete_policy ON api.products FOR DELETE " + "USING (tenant_id = ANY(string_to_array(current_setting('request.jwt.claims', true)::json->'app_metadata'->>'tenant_ids', ',')::text[]))" ); log.debug("Created product RLS policies"); } catch (Exception e) { log.error("Failed to create product RLS policies: {}", e.getMessage()); } } /** * RLS policies for orders table */ private void createOrderPolicies() { try { jdbcTemplate.execute("DROP POLICY IF EXISTS orders_select_policy ON api.orders"); jdbcTemplate.execute("DROP POLICY IF EXISTS orders_insert_policy ON api.orders"); jdbcTemplate.execute("DROP POLICY IF EXISTS orders_update_policy ON api.orders"); // Users can read their own orders jdbcTemplate.execute( "CREATE POLICY orders_select_policy ON api.orders FOR SELECT " + "USING (user_id = current_setting('request.jwt.claims', true)::json->>'user_id'::bigint " + "OR tenant_id = ANY(string_to_array(current_setting('request.jwt.claims', true)::json->'app_metadata'->>'tenant_ids', ',')::text[]))" ); // Users can insert their own orders jdbcTemplate.execute( "CREATE POLICY orders_insert_policy ON api.orders FOR INSERT " + "WITH CHECK (user_id = current_setting('request.jwt.claims', true)::json->>'user_id'::bigint)" ); log.debug("Created order RLS policies"); } catch (Exception e) { log.error("Failed to create order RLS policies: {}", e.getMessage()); } } /** * RLS policies for tenants table */ private void createTenantPolicies() { try { jdbcTemplate.execute("DROP POLICY IF EXISTS tenants_select_policy ON api.tenants"); // Users can read tenants they have access to jdbcTemplate.execute( "CREATE POLICY tenants_select_policy ON api.tenants FOR SELECT " + "USING (id::text = ANY(string_to_array(current_setting('request.jwt.claims', true)::json->'app_metadata'->>'tenant_ids', ',')::text[]))" ); log.debug("Created tenant RLS policies"); } catch (Exception e) { log.error("Failed to create tenant RLS policies: {}", e.getMessage()); } } /** * Get current RLS policies */ public List<Map<String, Object>> getCurrentPolicies() { return jdbcTemplate.queryForList( "SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check " + "FROM pg_policies " + "WHERE schemaname = 'api' " + "ORDER BY tablename, policyname" ); } /** * Create custom RLS policy */ public void createCustomPolicy(String tableName, String policyName, String operation, String usingClause, String withCheckClause) { try { String sql = String.format( "CREATE POLICY %s ON api.%s FOR %s " + "USING (%s) %s", policyName, tableName, operation, usingClause, withCheckClause != null ? "WITH CHECK (" + withCheckClause + ")" : "" ); jdbcTemplate.execute(sql); log.info("Created custom RLS policy: {} on table {}", policyName, tableName); } catch (Exception e) { log.error("Failed to create custom RLS policy: {}", e.getMessage()); throw new RuntimeException("Failed to create RLS policy", e); } } }

API Gateway Security

// ApiGatewaySecurityService.java package com.example.postgrestsecurity.service; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import jakarta.servlet.http.HttpServletRequest; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @Slf4j @Service public class ApiGatewaySecurityService { private final Set<String> allowedIps; private final boolean ipWhitelistEnabled; private final int rateLimit; private final int burstCapacity; // Rate limiting storage private final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, Long> lastResetTimes = new ConcurrentHashMap<>(); public ApiGatewaySecurityService( @Value("${app.security.gateway.ip-whitelist.enabled:false}") boolean ipWhitelistEnabled, @Value("${app.security.gateway.ip-whitelist.allowed-ips:}") String allowedIpsConfig, @Value("${app.security.gateway.rate-limit.requests-per-minute:100}") int rateLimit, @Value("${app.security.gateway.rate-limit.burst-capacity:20}") int burstCapacity) { this.ipWhitelistEnabled = ipWhitelistEnabled; this.rateLimit = rateLimit; this.burstCapacity = burstCapacity; this.allowedIps = parseAllowedIps(allowedIpsConfig); } /** * Validate client IP address */ public boolean isIpAllowed(HttpServletRequest request) { if (!ipWhitelistEnabled) { return true; } String clientIp = getClientIpAddress(request); boolean allowed = isIpInWhitelist(clientIp); if (!allowed) { log.warn("IP address blocked: {}", clientIp); } return allowed; } /** * Check rate limiting */ public boolean isRateLimited(HttpServletRequest request) { String clientId = getClientIdentifier(request); long currentTime = System.currentTimeMillis(); long oneMinuteAgo = currentTime - 60000; // Clean up old entries lastResetTimes.entrySet().removeIf(entry -> entry.getValue() < oneMinuteAgo); requestCounts.entrySet().removeIf(entry -> !lastResetTimes.containsKey(entry.getKey())); // Get or create request counter AtomicInteger counter = requestCounts.computeIfAbsent(clientId, k -> new AtomicInteger(0)); Long lastReset = lastResetTimes.computeIfAbsent(clientId, k -> currentTime); // Reset counter if more than a minute has passed if (currentTime - lastReset > 60000) { counter.set(0); lastResetTimes.put(clientId, currentTime); } // Check rate limit int currentCount = counter.incrementAndGet(); boolean limited = currentCount > rateLimit; if (limited) { log.warn("Rate limit exceeded for client: {} - {} requests", clientId, currentCount); } return limited; } /** * Validate request headers */ public boolean validateHeaders(HttpServletRequest request) { // Check for required headers String userAgent = request.getHeader("User-Agent"); if (userAgent == null || userAgent.trim().isEmpty()) { log.warn("Request blocked: missing User-Agent header"); return false; } // Check content type for POST/PUT requests String method = request.getMethod(); if ("POST".equals(method) || "PUT".equals(method)) { String contentType = request.getContentType(); if (contentType == null || (!contentType.contains("application/json") && !contentType.contains("application/x-www-form-urlencoded"))) { log.warn("Request blocked: invalid Content-Type - {}", contentType); return false; } } return true; } /** * Get client IP address */ private String getClientIpAddress(HttpServletRequest request) { String xForwardedFor = request.getHeader("X-Forwarded-For"); if (xForwardedFor != null && !xForwardedFor.isEmpty()) { return xForwardedFor.split(",")[0].trim(); } String xRealIp = request.getHeader("X-Real-IP"); if (xRealIp != null && !xRealIp.isEmpty()) { return xRealIp; } return request.getRemoteAddr(); } /** * Get client identifier for rate limiting */ private String getClientIdentifier(HttpServletRequest request) { // Use API key if available, otherwise use IP address String apiKey = request.getHeader("X-API-Key"); if (apiKey != null && !apiKey.isEmpty()) { return "api_key:" + apiKey; } return "ip:" + getClientIpAddress(request); } /** * Check if IP is in whitelist */ private boolean isIpInWhitelist(String ip) { if (allowedIps.isEmpty()) { return true; // Allow all if no whitelist configured } // Simple CIDR matching (for production, use a proper IP matching library) for (String allowedIp : allowedIps) { if (allowedIp.contains("/")) { // CIDR notation - simplified check if (isIpInCidr(ip, allowedIp)) { return true; } } else { // Exact IP match if (ip.equals(allowedIp)) { return true; } } } return false; } /** * Simple CIDR matching (for production, use a proper library) */ private boolean isIpInCidr(String ip, String cidr) { try { String[] parts = cidr.split("/"); String network = parts[0]; int prefix = Integer.parseInt(parts[1]); // Simplified implementation - for production use a proper IP math library return ip.startsWith(network.substring(0, network.lastIndexOf('.'))); } catch (Exception e) { log.warn("Invalid CIDR notation: {}", cidr); return false; } } /** * Parse allowed IPs from configuration */ private Set<String> parseAllowedIps(String allowedIpsConfig) { Set<String> ips = new HashSet<>(); if (allowedIpsConfig != null && !allowedIpsConfig.trim().isEmpty()) { String[] ipArray = allowedIpsConfig.split(","); for (String ip : ipArray) { ips.add(ip.trim()); } } return ips; } /** * Get security metrics */ public SecurityMetrics getSecurityMetrics() { return SecurityMetrics.builder() .totalClients(requestCounts.size()) .ipWhitelistEnabled(ipWhitelistEnabled) .allowedIpsCount(allowedIps.size()) .rateLimit(rateLimit) .burstCapacity(burstCapacity) .build(); } @lombok.Data @lombok.Builder public static class SecurityMetrics { private int totalClients; private boolean ipWhitelistEnabled; private int allowedIpsCount; private int rateLimit; private int burstCapacity; } }

PostgREST Proxy Controller

// PostgRESTProxyController.java package com.example.postgrestsecurity.controller; import com.example.postgrestsecurity.service.ApiGatewaySecurityService; import com.example.postgrestsecurity.service.AuthenticationService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import jakarta.servlet.http.HttpServletRequest; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; @Slf4j @RestController @RequestMapping("/api/proxy") public class PostgRESTProxyController { private final RestTemplate restTemplate; private final AuthenticationService authenticationService; private final ApiGatewaySecurityService apiGatewaySecurityService; @Value("${app.security.postgrest.base-url:http://localhost:3000}") private String postgrestBaseUrl; public PostgRESTProxyController(AuthenticationService authenticationService, ApiGatewaySecurityService apiGatewaySecurityService) { this.restTemplate = new RestTemplate(); this.authenticationService = authenticationService; this.apiGatewaySecurityService = apiGatewaySecurityService; } /** * Proxy requests to PostgREST with security checks */ @RequestMapping("/**") public ResponseEntity<String> proxyRequest(HttpServletRequest request) { try { // 1. API Gateway Security Checks if (!apiGatewaySecurityService.validateHeaders(request)) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body("{\"error\": \"Invalid request headers\"}"); } if (!apiGatewaySecurityService.isIpAllowed(request)) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body("{\"error\": \"IP address not allowed\"}"); } if (apiGatewaySecurityService.isRateLimited(request)) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS) .body("{\"error\": \"Rate limit exceeded\"}"); } // 2. Authentication Check String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body("{\"error\": \"Authentication required\"}"); } String token = authHeader.substring(7); if (!authenticationService.validateToken(token)) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body("{\"error\": \"Invalid or expired token\"}"); } // 3. Build PostgREST request String path = extractPath(request); String url = postgrestBaseUrl + path; HttpMethod method = HttpMethod.valueOf(request.getMethod()); HttpHeaders headers = buildPostgRESTHeaders(request, token); String body = extractRequestBody(request); HttpEntity<String> entity = new HttpEntity<>(body, headers); // 4. Forward request to PostgREST ResponseEntity<String> response = restTemplate.exchange(url, method, entity, String.class); log.debug("Proxied request to PostgREST: {} {} - Status: {}", method, path, response.getStatusCode()); return response; } catch (Exception e) { log.error("Proxy request failed", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("{\"error\": \"Internal server error: " + e.getMessage() + "\"}"); } } /** * Extract path from request */ private String extractPath(HttpServletRequest request) { String path = request.getRequestURI(); String contextPath = request.getContextPath(); String proxyPath = "/api/proxy"; if (path.startsWith(contextPath + proxyPath)) { return path.substring((contextPath + proxyPath).length()); } return path; } /** * Build headers for PostgREST */ private HttpHeaders buildPostgRESTHeaders(HttpServletRequest request, String token) { HttpHeaders headers = new HttpHeaders(); // Copy relevant headers Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); if (shouldForwardHeader(headerName)) { String headerValue = request.getHeader(headerName); headers.add(headerName, headerValue); } } // Set PostgREST specific headers headers.set("Authorization", "Bearer " + token); headers.set("Accept", "application/json"); // Set Prefer headers for PostgREST String preferHeader = request.getHeader("Prefer"); if (preferHeader != null) { headers.set("Prefer", preferHeader); } return headers; } /** * Determine which headers to forward */ private boolean shouldForwardHeader(String headerName) { String lowerHeader = headerName.toLowerCase(); // Don't forward these headers if (lowerHeader.equals("host") || lowerHeader.equals("authorization") || lowerHeader.equals("content-length") || lowerHeader.startsWith("x-forwarded-") || lowerHeader.startsWith("x-real-")) { return false; } // Forward content-related headers if (lowerHeader.startsWith("content-") || lowerHeader.startsWith("accept") || lowerHeader.startsWith("prefer")) { return true; } return true; } /** * Extract request body */ private String extractRequestBody(HttpServletRequest request) { // For simplicity, this is a basic implementation // In production, you would need to read the request body properly try { if ("POST".equals(request.getMethod()) || "PUT".equals(request.getMethod()) || "PATCH".equals(request.getMethod())) { // You would need to cache the request body to read it multiple times // This is a simplified version return ""; // Implement proper body extraction } } catch (Exception e) { log.warn("Failed to extract request body", e); } return null; } /** * Health check endpoint */ @GetMapping("/health") public ResponseEntity<Map<String, Object>> health() { Map<String, Object> health = new HashMap<>(); health.put("status", "healthy"); health.put("service", "postgrest-proxy"); health.put("timestamp", java.time.LocalDateTime.now()); try { // Test PostgREST connection ResponseEntity<String> response = restTemplate.getForEntity(postgrestBaseUrl, String.class); health.put("postgrest", response.getStatusCode().is2xxSuccess() ? "connected" : "error"); } catch (Exception e) { health.put("postgrest", "disconnected"); } return ResponseEntity.ok(health); } }

Security Configuration

// SecurityConfig.java package com.example.postgrestsecurity.config; import com.example.postgrestsecurity.service.AuthenticationService; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; import java.util.List; @Slf4j @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(authz -> authz // Public endpoints .requestMatchers("/api/auth/**", "/api/proxy/health", "/actuator/health").permitAll() // Secure proxy endpoints .requestMatchers("/api/proxy/**").authenticated() // All other endpoints require authentication .anyRequest().authenticated() ) .exceptionHandling(exceptions -> exceptions .authenticationEntryPoint((request, response, authException) -> { log.warn("Authentication failed: {}", authException.getMessage()); response.setStatus(401); response.setContentType("application/json"); response.getWriter().write("{\"error\": \"Authentication required\"}"); }) .accessDeniedHandler((request, response, accessDeniedException) -> { log.warn("Access denied: {}", accessDeniedException.getMessage()); response.setStatus(403); response.setContentType("application/json"); response.getWriter().write("{\"error\": \"Access denied\"}"); }) ); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOriginPatterns(List.of("*")); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("authorization", "content-type", "x-requested-with", "prefer", "accept", "accept-profile")); configuration.setExposedHeaders(Arrays.asList("content-range", "location")); configuration.setAllowCredentials(true); configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }

Authentication Controller

// AuthController.java package com.example.postgrestsecurity.controller; import com.example.postgrestsecurity.model.AuthRequest; import com.example.postgrestsecurity.model.AuthResponse; import com.example.postgrestsecurity.service.AuthenticationService; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; import java.util.Map; @Slf4j @RestController @RequestMapping("/api/auth") public class AuthController { private final AuthenticationService authenticationService; public AuthController(AuthenticationService authenticationService) { this.authenticationService = authenticationService; } @PostMapping("/login") public ResponseEntity<AuthResponse> login(@Valid @RequestBody AuthRequest authRequest) { log.info("Login attempt for user: {}", authRequest.getUsername()); AuthResponse response = authenticationService.authenticate(authRequest); if (response.isSuccess()) { return ResponseEntity.ok(response); } else { return ResponseEntity.status(401).body(response); } } @PostMapping("/validate") public ResponseEntity<Map<String, Object>> validateToken(@RequestHeader("Authorization") String authHeader) { if (authHeader == null || !authHeader.startsWith("Bearer ")) { return ResponseEntity.badRequest().body(Map.of("valid", false, "error", "Invalid authorization header")); } String token = authHeader.substring(7); boolean isValid = authenticationService.validateToken(token); return ResponseEntity.ok(Map.of("valid", isValid)); } @PostMapping("/refresh") public ResponseEntity<Map<String, Object>> refreshToken(@RequestHeader("Authorization") String authHeader) { // Implementation for token refresh return ResponseEntity.status(501).body(Map.of("error", "Not implemented")); } @PostMapping("/logout") public ResponseEntity<Map<String, Object>> logout(@RequestHeader("Authorization") String authHeader) { // In a stateless JWT system, logout is handled client-side // You might want to maintain a blacklist for logged out tokens log.info("Logout requested"); return ResponseEntity.ok(Map.of("message", "Logout successful")); } }

Testing

1. Unit Tests
// AuthenticationServiceTest.java package com.example.postgrestsecurity.service; import com.example.postgrestsecurity.entity.User; import com.example.postgrestsecurity.model.AuthRequest; import com.example.postgrestsecurity.model.AuthResponse; import com.example.postgrestsecurity.repository.UserRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.Optional; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class AuthenticationServiceTest { @Mock private UserRepository userRepository; @Mock private JwtTokenService jwtTokenService; @Mock private PasswordEncoder passwordEncoder; @InjectMocks private AuthenticationService authenticationService; @Test void testAuthenticate_Success() { // Setup AuthRequest authRequest = new AuthRequest(); authRequest.setUsername("testuser"); authRequest.setPassword("password"); User user = new User(); user.setId(1L); user.setUsername("testuser"); user.setEmail("[email protected]"); user.setIsActive(true); user.setPasswordHash("hashedpassword"); when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(user)); when(passwordEncoder.matches("password", "hashedpassword")).thenReturn(true); when(jwtTokenService.generatePostgRESTToken(any(User.class))).thenReturn("test.jwt.token"); // Execute AuthResponse response = authenticationService.authenticate(authRequest); // Verify assertTrue(response.isSuccess()); assertEquals("test.jwt.token", response.getAccessToken()); verify(userRepository).save(user); // Verify last login update } @Test void testAuthenticate_InvalidCredentials() { // Setup AuthRequest authRequest = new AuthRequest(); authRequest.setUsername("testuser"); authRequest.setPassword("wrongpassword"); User user = new User(); user.setUsername("testuser"); user.setPasswordHash("hashedpassword"); user.setIsActive(true); when(userRepository.findByUsername("testuser")).thenReturn(Optional.of(user)); when(passwordEncoder.matches("wrongpassword", "hashedpassword")).thenReturn(false); // Execute AuthResponse response = authenticationService.authenticate(authRequest); // Verify assertFalse(response.isSuccess()); assertEquals("Invalid credentials", response.getMessage()); } } // JwtTokenServiceTest.java package com.example.postgrestsecurity.service; import com.example.postgrestsecurity.entity.User; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashSet; import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockitoExtension.class) class JwtTokenServiceTest { private final JwtTokenService jwtTokenService = new JwtTokenService( "test-secret-key-that-is-long-enough-for-hs256-32-chars", 3600, "test-issuer" ); @Test void testGenerateAndValidateToken() { // Setup User user = new User(); user.setId(1L); user.setUsername("testuser"); user.setEmail("[email protected]"); user.setTenantIds(new HashSet<>()); // Execute var token = jwtTokenService.generateToken(user); var claims = jwtTokenService.validateToken(token.getToken()); // Verify assertNotNull(token); assertNotNull(claims); assertEquals("1", claims.getSubject()); assertEquals("testuser", claims.getUsername()); } }
2. Integration Test
// PostgRESTSecurityIntegrationTest.java package com.example.postgrestsecurity.integration; import com.example.postgrestsecurity.controller.AuthController; import com.example.postgrestsecurity.model.AuthRequest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @ActiveProfiles("test") class PostgRESTSecurityIntegrationTest { @Autowired private AuthController authController; @Test void testAuthenticationWorkflow() { // Setup AuthRequest authRequest = new AuthRequest(); authRequest.setUsername("testuser"); authRequest.setPassword("testpassword"); // Execute var response = authController.login(authRequest); // Verify assertTrue(response.getStatusCode().is2xxSuccessful()); // Add more assertions based on your test data } }

Production Considerations

1. Database Configuration
-- Production database setup -- Enable required extensions CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Set secure parameters ALTER SYSTEM SET shared_preload_libraries = 'pg_stat_statements'; ALTER SYSTEM SET max_connections = 200; ALTER SYSTEM SET shared_buffers = '1GB'; ALTER SYSTEM SET work_mem = '16MB'; -- Security parameters ALTER SYSTEM SET password_encryption = 'scram-sha-256'; ALTER SYSTEM SET ssl = on;
2. Monitoring and Logging
// SecurityMonitoringService.java package com.example.postgrestsecurity.service; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.concurrent.atomic.AtomicLong; @Slf4j @Service public class SecurityMonitoringService { private final AtomicLong totalRequests = new AtomicLong(0); private final AtomicLong failedAuthAttempts = new AtomicLong(0); private final AtomicLong blockedRequests = new AtomicLong(0); public void recordRequest() { totalRequests.incrementAndGet(); } public void recordFailedAuth() { failedAuthAttempts.incrementAndGet(); } public void recordBlockedRequest() { blockedRequests.incrementAndGet(); } @Scheduled(fixedRate = 60000) // Every minute public void logSecurityMetrics() { log.info("Security Metrics - Total: {}, Failed Auth: {}, Blocked: {}", totalRequests.get(), failedAuthAttempts.get(), blockedRequests.get()); // Reset counters totalRequests.set(0); failedAuthAttempts.set(0); blockedRequests.set(0); } }

Best Practices

  1. Security:
  • Use strong JWT secrets (minimum 32 characters)
  • Implement proper token expiration and refresh
  • Enable SSL/TLS for all communications
  • Regular security audits and penetration testing
  1. Performance:
  • Implement connection pooling for database
  • Use caching for frequently accessed data
  • Monitor and optimize RLS policies
  • Implement rate limiting appropriately
  1. Monitoring:
  • Log all authentication attempts
  • Monitor RLS policy performance
  • Track API usage patterns
  • Set up alerts for security events
  1. Database:
  • Regular backups with point-in-time recovery
  • Database replication for high availability
  • Regular vacuum and analyze operations
  • Monitor long-running queries

Conclusion

This PostgREST security implementation provides:

  • Comprehensive authentication with JWT tokens
  • Fine-grained authorization with Row Level Security
  • API gateway security with rate limiting and IP whitelisting
  • Multi-tenant support with tenant isolation
  • Production-ready features including monitoring and logging

The solution can be extended with:

  • OAuth2/OIDC integration
  • Advanced rate limiting strategies
  • Real-time security monitoring
  • Automated security policy management
  • Integration with enterprise identity providers

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper