Modern Backend-as-a-Service Auth: Integrating Supabase Authentication with Java Applications


Article

In the era of rapid application development, managing authentication can be complex and time-consuming. Supabase provides an open-source Firebase alternative with built-in authentication, while Java remains the backbone of enterprise applications. Combining Supabase's modern auth capabilities with Java's robustness creates a powerful stack for secure, scalable applications without the overhead of building authentication systems from scratch.

What is Supabase Auth?

Supabase Auth is a complete authentication system that provides:

  • Multiple Auth Providers: Email/password, OAuth (Google, GitHub, etc.), magic links
  • JWT Management: Automatic token generation and refresh
  • User Management: Built-in user profiles and sessions
  • Row Level Security: PostgreSQL policies for data access control
  • Server-Side APIs: RESTful endpoints for auth operations

Why Supabase Auth for Java Applications?

  1. Rapid Development: Implement auth in hours, not weeks
  2. Security Best Practices: Built-in protection against common vulnerabilities
  3. Scalability: Handles millions of users with Supabase's infrastructure
  4. Cost-Effective: Open-source with generous free tier
  5. Modern Standards: OAuth 2.0, OpenID Connect, and JWT compliance

Supabase Auth Architecture for Java

Frontend → Supabase Auth API → Java Backend → Supabase PostgreSQL ↓ JWT Tokens → Spring Security → Protected Endpoints

Setting Up Supabase for Java

1. Create Supabase Project:

  • Go to supabase.com and create a new project
  • Note your Project URL and anon/service_role keys
  • Configure auth providers in the Authentication settings

2. Add Dependencies:

<!-- pom.xml --> <dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- JWT Support --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <!-- HTTP Client --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- Configuration --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies>

Configuration

1. Application Properties:

# application.yml supabase: url: ${SUPABASE_URL:https://your-project.supabase.co} anon-key: ${SUPABASE_ANON_KEY:your-anon-key} service-key: ${SUPABASE_SERVICE_KEY:your-service-key} jwt: secret: ${SUPABASE_JWT_SECRET:your-jwt-secret} issuer: ${SUPABASE_URL:https://your-project.supabase.co}/auth/v1 audience: authenticator app: security: allowed-origins: "http://localhost:3000,https://your-app.vercel.app"

2. Supabase Configuration Properties:

@Configuration @ConfigurationProperties(prefix = "supabase") @Data public class SupabaseProperties { private String url; private String anonKey; private String serviceKey; private JwtProperties jwt; @Data public static class JwtProperties { private String secret; private String issuer; private String audience; } }

Supabase Auth Service

1. Core Auth Service:

@Service @Slf4j public class SupabaseAuthService { private final SupabaseProperties properties; private final WebClient webClient; private final JwtService jwtService; public SupabaseAuthService(SupabaseProperties properties, JwtService jwtService) { this.properties = properties; this.jwtService = jwtService; this.webClient = WebClient.builder() .baseUrl(properties.getUrl() + "/auth/v1") .defaultHeader("apikey", properties.getAnonKey()) .defaultHeader("Authorization", "Bearer " + properties.getAnonKey()) .build(); } public AuthResponse signUpWithEmail(SignUpRequest request) { try { Map<String, Object> body = new HashMap<>(); body.put("email", request.getEmail()); body.put("password", request.getPassword()); body.put("data", request.getUserMetadata()); return webClient.post() .uri("/signup") .contentType(MediaType.APPLICATION_JSON) .bodyValue(body) .retrieve() .bodyToMono(AuthResponse.class) .block(); } catch (WebClientResponseException e) { log.error("Sign up failed: {}", e.getResponseBodyAsString()); throw new AuthException("Sign up failed: " + e.getMessage()); } } public AuthResponse signInWithEmail(SignInRequest request) { try { Map<String, Object> body = new HashMap<>(); body.put("email", request.getEmail()); body.put("password", request.getPassword()); return webClient.post() .uri("/token?grant_type=password") .contentType(MediaType.APPLICATION_JSON) .bodyValue(body) .retrieve() .bodyToMono(AuthResponse.class) .block(); } catch (WebClientResponseException e) { log.error("Sign in failed: {}", e.getResponseBodyAsString()); throw new AuthException("Sign in failed: " + e.getMessage()); } } public AuthResponse signInWithOAuth(String provider, String redirectTo) { try { Map<String, Object> body = new HashMap<>(); body.put("provider", provider); body.put("redirect_to", redirectTo); return webClient.post() .uri("/authorize") .contentType(MediaType.APPLICATION_JSON) .bodyValue(body) .retrieve() .bodyToMono(AuthResponse.class) .block(); } catch (WebClientResponseException e) { log.error("OAuth sign in failed: {}", e.getResponseBodyAsString()); throw new AuthException("OAuth sign in failed: " + e.getMessage()); } } public User getCurrentUser(String accessToken) { try { return webClient.get() .uri("/user") .header("Authorization", "Bearer " + accessToken) .retrieve() .bodyToMono(User.class) .block(); } catch (WebClientResponseException e) { log.error("Get user failed: {}", e.getResponseBodyAsString()); throw new AuthException("Get user failed: " + e.getMessage()); } } public void signOut(String accessToken) { try { webClient.post() .uri("/logout") .header("Authorization", "Bearer " + accessToken) .retrieve() .toBodilessEntity() .block(); } catch (WebClientResponseException e) { log.error("Sign out failed: {}", e.getResponseBodyAsString()); throw new AuthException("Sign out failed: " + e.getMessage()); } } public AuthResponse refreshToken(String refreshToken) { try { Map<String, Object> body = new HashMap<>(); body.put("refresh_token", refreshToken); return webClient.post() .uri("/token?grant_type=refresh_token") .contentType(MediaType.APPLICATION_JSON) .bodyValue(body) .retrieve() .bodyToMono(AuthResponse.class) .block(); } catch (WebClientResponseException e) { log.error("Token refresh failed: {}", e.getResponseBodyAsString()); throw new AuthException("Token refresh failed: " + e.getMessage()); } } public boolean validateToken(String token) { return jwtService.validateToken(token); } public SupabaseUser extractUserFromToken(String token) { return jwtService.extractUser(token); } }

2. JWT Service for Token Validation:

@Service @Slf4j public class JwtService { private final SupabaseProperties properties; private final JwtParser jwtParser; public JwtService(SupabaseProperties properties) { this.properties = properties; // Supabase uses HS256 for JWT signing SecretKey key = Keys.hmacShaKeyFor( properties.getJwt().getSecret().getBytes(StandardCharsets.UTF_8) ); this.jwtParser = Jwts.parserBuilder() .setSigningKey(key) .requireIssuer(properties.getJwt().getIssuer()) .requireAudience(properties.getJwt().getAudience()) .build(); } public boolean validateToken(String token) { try { jwtParser.parseClaimsJws(token); return true; } catch (JwtException e) { log.warn("Invalid JWT token: {}", e.getMessage()); return false; } } public SupabaseUser extractUser(String token) { try { Claims claims = jwtParser.parseClaimsJws(token).getBody(); return SupabaseUser.builder() .id(claims.getSubject()) .email(claims.get("email", String.class)) .phone(claims.get("phone", String.class)) .role(claims.get("role", String.class)) .appMetadata(claims.get("app_metadata", Map.class)) .userMetadata(claims.get("user_metadata", Map.class)) .build(); } catch (JwtException e) { throw new AuthException("Failed to extract user from token: " + e.getMessage()); } } public String extractUserId(String token) { return extractUser(token).getId(); } public Date extractExpiration(String token) { try { Claims claims = jwtParser.parseClaimsJws(token).getBody(); return claims.getExpiration(); } catch (JwtException e) { throw new AuthException("Failed to extract expiration from token"); } } public boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } }

Data Models

1. Request/Response Models:

@Data @Builder @NoArgsConstructor @AllArgsConstructor public class SignUpRequest { @Email @NotBlank private String email; @NotBlank @Size(min = 6) private String password; private Map<String, Object> userMetadata; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class SignInRequest { @Email @NotBlank private String email; @NotBlank private String password; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class AuthResponse { private User user; private String accessToken; private String refreshToken; private String tokenType; private Integer expiresIn; private String error; private String errorDescription; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class SupabaseUser { private String id; private String email; private String phone; private String role; private String aud; private Map<String, Object> appMetadata; private Map<String, Object> userMetadata; private Date createdAt; } // Supabase API Response Models @Data public class User { private String id; private String aud; private String role; private String email; private String phone; private String confirmationSentAt; private String confirmedAt; private String emailConfirmedAt; private String phoneConfirmedAt; private String lastSignInAt; private String appMetadata; private String userMetadata; private String identities; private String createdAt; private String updatedAt; }

Spring Security Integration

1. JWT Authentication Filter:

@Component @Slf4j public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtService jwtService; private final SupabaseAuthService authService; public JwtAuthenticationFilter(JwtService jwtService, SupabaseAuthService authService) { this.jwtService = jwtService; this.authService = authService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = extractToken(request); if (token != null && jwtService.validateToken(token)) { try { SupabaseUser user = jwtService.extractUser(token); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, getAuthorities(user)); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); log.debug("Authenticated user: {}", user.getEmail()); } catch (AuthException e) { log.warn("Failed to authenticate user from token: {}", e.getMessage()); SecurityContextHolder.clearContext(); } } filterChain.doFilter(request, response); } private String extractToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } // Also check for token in query parameter (for WebSocket connections) String queryToken = request.getParameter("token"); if (StringUtils.hasText(queryToken)) { return queryToken; } return null; } private Collection<? extends GrantedAuthority> getAuthorities(SupabaseUser user) { List<GrantedAuthority> authorities = new ArrayList<>(); // Add role-based authority if (user.getRole() != null) { authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole().toUpperCase())); } // Add additional authorities from app_metadata if (user.getAppMetadata() != null && user.getAppMetadata().containsKey("authorities")) { Object auths = user.getAppMetadata().get("authorities"); if (auths instanceof List) { ((List<?>) auths).forEach(auth -> authorities.add(new SimpleGrantedAuthority(auth.toString())) ); } } return authorities; } }

2. Security Configuration:

@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) @RequiredArgsConstructor public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final SupabaseProperties supabaseProperties; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(cors -> cors.configurationSource(corsConfigurationSource())) .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(authz -> authz .requestMatchers("/auth/**", "/public/**", "/health").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationToken.class); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList( supabaseProperties.getUrl(), "http://localhost:3000", "https://your-app.vercel.app" )); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @Bean public PasswordEncoder passwordEncoder() { // Note: Supabase handles password hashing, this is for any local password needs return new BCryptPasswordEncoder(); } }

REST Controller

1. Auth Controller:

@RestController @RequestMapping("/auth") @Slf4j @Validated public class AuthController { private final SupabaseAuthService authService; public AuthController(SupabaseAuthService authService) { this.authService = authService; } @PostMapping("/signup") public ResponseEntity<AuthResponse> signUp(@Valid @RequestBody SignUpRequest request) { try { AuthResponse response = authService.signUpWithEmail(request); return ResponseEntity.ok(response); } catch (AuthException e) { return ResponseEntity.badRequest().body( AuthResponse.builder() .error("SIGNUP_FAILED") .errorDescription(e.getMessage()) .build() ); } } @PostMapping("/signin") public ResponseEntity<AuthResponse> signIn(@Valid @RequestBody SignInRequest request) { try { AuthResponse response = authService.signInWithEmail(request); return ResponseEntity.ok(response); } catch (AuthException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( AuthResponse.builder() .error("SIGNIN_FAILED") .errorDescription(e.getMessage()) .build() ); } } @PostMapping("/oauth/{provider}") public ResponseEntity<AuthResponse> signInWithOAuth( @PathVariable String provider, @RequestParam String redirectTo) { try { AuthResponse response = authService.signInWithOAuth(provider, redirectTo); return ResponseEntity.ok(response); } catch (AuthException e) { return ResponseEntity.badRequest().body( AuthResponse.builder() .error("OAUTH_FAILED") .errorDescription(e.getMessage()) .build() ); } } @PostMapping("/signout") public ResponseEntity<Void> signOut(@RequestHeader("Authorization") String authorization) { try { String token = authorization.substring(7); // Remove "Bearer " prefix authService.signOut(token); return ResponseEntity.ok().build(); } catch (AuthException e) { return ResponseEntity.badRequest().build(); } } @PostMapping("/refresh") public ResponseEntity<AuthResponse> refreshToken(@RequestParam String refreshToken) { try { AuthResponse response = authService.refreshToken(refreshToken); return ResponseEntity.ok(response); } catch (AuthException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body( AuthResponse.builder() .error("REFRESH_FAILED") .errorDescription(e.getMessage()) .build() ); } } @GetMapping("/me") public ResponseEntity<SupabaseUser> getCurrentUser(@AuthenticationPrincipal SupabaseUser user) { return ResponseEntity.ok(user); } @GetMapping("/validate") public ResponseEntity<Boolean> validateToken(@RequestParam String token) { boolean isValid = authService.validateToken(token); return ResponseEntity.ok(isValid); } }

2. Protected Resource Controller:

@RestController @RequestMapping("/api") @Slf4j public class ProtectedController { @GetMapping("/user/profile") public ResponseEntity<Map<String, Object>> getUserProfile(@AuthenticationPrincipal SupabaseUser user) { Map<String, Object> profile = new HashMap<>(); profile.put("id", user.getId()); profile.put("email", user.getEmail()); profile.put("role", user.getRole()); profile.put("userMetadata", user.getUserMetadata()); profile.put("createdAt", user.getCreatedAt()); return ResponseEntity.ok(profile); } @PreAuthorize("hasRole('ADMIN')") @GetMapping("/admin/dashboard") public ResponseEntity<Map<String, String>> adminDashboard() { return ResponseEntity.ok(Collections.singletonMap("message", "Admin access granted")); } @PreAuthorize("hasRole('USER') or hasRole('ADMIN')") @PostMapping("/user/data") public ResponseEntity<Map<String, String>> userData(@RequestBody Map<String, Object> data, @AuthenticationPrincipal SupabaseUser user) { log.info("User {} submitted data: {}", user.getEmail(), data); return ResponseEntity.ok(Collections.singletonMap("status", "Data received")); } }

Error Handling

1. Custom Exception Handler:

@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler(AuthException.class) public ResponseEntity<ErrorResponse> handleAuthException(AuthException e) { log.warn("Authentication error: {}", e.getMessage()); ErrorResponse error = ErrorResponse.builder() .error("AUTH_ERROR") .message(e.getMessage()) .timestamp(Instant.now()) .build(); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException e) { ErrorResponse error = ErrorResponse.builder() .error("ACCESS_DENIED") .message("Insufficient permissions") .timestamp(Instant.now()) .build(); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error); } } @Data @Builder class ErrorResponse { private String error; private String message; private Instant timestamp; } class AuthException extends RuntimeException { public AuthException(String message) { super(message); } public AuthException(String message, Throwable cause) { super(message, cause); } }

Testing

1. Integration Tests:

@SpringBootTest @AutoConfigureTestDatabase @TestPropertySource(properties = { "supabase.url=https://test-project.supabase.co", "supabase.anon-key=test-anon-key", "supabase.jwt.secret=test-jwt-secret" }) public class SupabaseAuthIntegrationTest { @Autowired private SupabaseAuthService authService; @Autowired private JwtService jwtService; @Test void testTokenValidation() { // Given a valid JWT token String validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // When validating the token boolean isValid = jwtService.validateToken(validToken); // Then it should be valid assertThat(isValid).isTrue(); } @Test void testUserExtractionFromToken() { // Given a valid JWT token String validToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // When extracting user from token SupabaseUser user = jwtService.extractUser(validToken); // Then user details should be correct assertThat(user.getEmail()).isEqualTo("[email protected]"); assertThat(user.getId()).isNotNull(); } }

Best Practices for Production

1. Environment-Specific Configuration:

@Configuration @Profile("production") public class ProductionSecurityConfig { @Bean public SecurityFilterChain productionFilterChain(HttpSecurity http) throws Exception { return http .headers(headers -> headers .httpStrictTransportSecurity(hsts -> hsts .includeSubDomains(true) .maxAgeInSeconds(31536000) ) .contentSecurityPolicy(csp -> csp .policyDirectives("default-src 'self' https://*.supabase.co") ) .frameOptions(frame -> frame.deny()) ) .build(); } }

2. Rate Limiting:

@Component public class RateLimitingService { private final Map<String, RateLimit> rateLimits = new ConcurrentHashMap<>(); public boolean isAllowed(String identifier, int maxRequests, Duration duration) { RateLimit limit = rateLimits.computeIfAbsent(identifier, k -> new RateLimit(maxRequests, duration)); return limit.tryAcquire(); } @Data private static class RateLimit { private final int maxRequests; private final Duration duration; private final Queue<Long> requests = new LinkedList<>(); public synchronized boolean tryAcquire() { long now = System.currentTimeMillis(); long windowStart = now - duration.toMillis(); // Remove old requests while (!requests.isEmpty() && requests.peek() < windowStart) { requests.poll(); } // Check if under limit if (requests.size() < maxRequests) { requests.offer(now); return true; } return false; } } }

Conclusion

Supabase Auth provides a robust, scalable authentication solution that integrates seamlessly with Java applications. By leveraging Spring Security for JWT validation and Supabase's REST API for auth operations, Java developers can implement modern authentication features quickly while maintaining enterprise-grade security standards.

The combination of Supabase's managed auth service with Java's strong typing and Spring ecosystem creates a powerful foundation for building secure, production-ready applications. This approach eliminates the complexity of managing auth infrastructure while providing the flexibility to customize authentication flows for specific business needs.

Leave a Reply

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


Macro Nepal Helper