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