Traefik Forward Auth is an authentication middleware that delegates authentication to an external service. This guide provides a complete Java implementation for securing your applications behind Traefik.
Traefik Forward Auth Overview
How It Works:
- Traefik intercepts requests to protected services
- Redirects to auth service for authentication
- Auth service validates credentials and sets cookies
- Subsequent requests include auth cookies for validation
Key Features:
- Centralized authentication for multiple services
- Support for various auth providers (OAuth, OIDC, Basic Auth)
- Flexible rule-based authentication
- JWT token validation
Dependencies and Setup
Maven Dependencies
<properties> <spring-boot.version>3.1.0</spring-boot.version> <jjwt.version>0.11.5</jjwt.version> <lombok.version>1.18.28</lombok.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-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>${spring-boot.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> <version>${lombok.version}</version> <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: 8081 servlet: context-path: /auth app: auth: # Cookie settings cookie: name: "traefik-forward-auth" domain: ".example.com" secure: true http-only: true same-site: "lax" max-age: 86400 # 24 hours # JWT settings jwt: secret: "${JWT_SECRET:your-super-secret-jwt-key-here}" expiration: 3600 # 1 hour issuer: "traefik-forward-auth" # Allowed domains for redirects allowed-domains: - "example.com" - "app.example.com" # Whitelisted paths (no auth required) whitelist: - "/health" - "/metrics" # Providers providers: oidc: enabled: true issuer: "https://accounts.google.com" client-id: "${OIDC_CLIENT_ID}" client-secret: "${OIDC_CLIENT_SECRET}" scopes: "openid,email,profile" basic: enabled: false users: - username: "admin" password: "${ADMIN_PASSWORD}" spring: redis: host: localhost port: 6379 password: "" database: 0 logging: level: com.example.traefikauth: DEBUG Core Models
1. Authentication Models
// AuthRequest.java package com.example.traefikauth.model; import lombok.Data; import jakarta.validation.constraints.NotBlank; @Data public class AuthRequest { @NotBlank(message = "Client ID is required") private String clientId; private String redirectUri; private String state; private String code; private String scope; // Traefik specific headers private String forwardUri; private String forwardMethod; private String forwardHost; private String forwardProto; } // AuthResponse.java package com.example.traefikauth.model; import lombok.Data; import lombok.Builder; @Data @Builder public class AuthResponse { private boolean authenticated; private String redirectUrl; private String user; private String email; private String error; private Integer statusCode; // Headers to set in Traefik response private String setCookie; private String removeCookie; public static AuthResponse success(String user, String email) { return AuthResponse.builder() .authenticated(true) .user(user) .email(email) .build(); } public static AuthResponse redirect(String redirectUrl) { return AuthResponse.builder() .authenticated(false) .redirectUrl(redirectUrl) .build(); } public static AuthResponse error(String error, Integer statusCode) { return AuthResponse.builder() .authenticated(false) .error(error) .statusCode(statusCode) .build(); } } // UserSession.java package com.example.traefikauth.model; import lombok.Data; import lombok.Builder; import java.time.LocalDateTime; import java.util.Map; @Data @Builder public class UserSession { private String sessionId; private String userId; private String email; private String username; private Map<String, Object> claims; private LocalDateTime createdAt; private LocalDateTime expiresAt; private String provider; public boolean isValid() { return expiresAt != null && expiresAt.isAfter(LocalDateTime.now()); } } // JwtToken.java package com.example.traefikauth.model; import lombok.Data; import lombok.Builder; import java.time.LocalDateTime; import java.util.Map; @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()); } } 2. Configuration Models
// AuthConfig.java package com.example.traefikauth.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; @Data @Component @ConfigurationProperties(prefix = "app.auth") public class AuthConfig { private CookieConfig cookie = new CookieConfig(); private JwtConfig jwt = new JwtConfig(); private List<String> allowedDomains; private List<String> whitelist; private ProviderConfig providers = new ProviderConfig(); @Data public static class CookieConfig { private String name = "traefik-forward-auth"; private String domain; private boolean secure = true; private boolean httpOnly = true; private String sameSite = "lax"; private int maxAge = 86400; // 24 hours } @Data public static class JwtConfig { private String secret; private int expiration = 3600; private String issuer = "traefik-forward-auth"; } @Data public static class ProviderConfig { private OidcConfig oidc = new OidcConfig(); private BasicAuthConfig basic = new BasicAuthConfig(); @Data public static class OidcConfig { private boolean enabled = false; private String issuer; private String clientId; private String clientSecret; private String scopes = "openid,email,profile"; private String authorizationEndpoint; private String tokenEndpoint; private String userInfoEndpoint; } @Data public static class BasicAuthConfig { private boolean enabled = false; private List<BasicUser> users; @Data public static class BasicUser { private String username; private String password; } } } } JWT Token Service
// JwtTokenService.java package com.example.traefikauth.service; import com.example.traefikauth.config.AuthConfig; import com.example.traefikauth.model.JwtToken; import com.example.traefikauth.model.UserSession; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; 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; @Slf4j @Service public class JwtTokenService { private final AuthConfig authConfig; private final SecretKey secretKey; public JwtTokenService(AuthConfig authConfig) { this.authConfig = authConfig; this.secretKey = Keys.hmacShaKeyFor(authConfig.getJwt().getSecret().getBytes()); } /** * Generate JWT token for user session */ public JwtToken generateToken(UserSession session) { LocalDateTime now = LocalDateTime.now(); LocalDateTime expiresAt = now.plusSeconds(authConfig.getJwt().getExpiration()); Map<String, Object> claims = new HashMap<>(); claims.put("sessionId", session.getSessionId()); claims.put("userId", session.getUserId()); claims.put("email", session.getEmail()); claims.put("username", session.getUsername()); claims.put("provider", session.getProvider()); if (session.getClaims() != null) { claims.putAll(session.getClaims()); } String token = Jwts.builder() .setIssuer(authConfig.getJwt().getIssuer()) .setSubject(session.getUserId()) .setAudience("traefik-forward-auth") .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(); } /** * Validate and parse JWT token */ public JwtToken validateToken(String token) { try { Claims claims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); LocalDateTime expiresAt = claims.getExpiration().toInstant() .atZone(ZoneId.systemDefault()) .toLocalDateTime(); Map<String, Object> claimsMap = new HashMap<>(claims); return JwtToken.builder() .token(token) .issuedAt(claims.getIssuedAt().toInstant() .atZone(ZoneId.systemDefault()) .toLocalDateTime()) .expiresAt(expiresAt) .claims(claimsMap) .build(); } catch (ExpiredJwtException e) { log.warn("JWT token expired: {}", e.getMessage()); throw new AuthException("Token expired", e); } catch (MalformedJwtException e) { log.warn("Invalid JWT token: {}", e.getMessage()); throw new AuthException("Invalid token", e); } catch (JwtException e) { log.error("JWT validation error: {}", e.getMessage()); throw new AuthException("Token validation failed", e); } } /** * Extract user session from JWT token */ public UserSession extractSession(String token) { JwtToken jwtToken = validateToken(token); return UserSession.builder() .sessionId((String) jwtToken.getClaims().get("sessionId")) .userId((String) jwtToken.getClaims().get("userId")) .email((String) jwtToken.getClaims().get("email")) .username((String) jwtToken.getClaims().get("username")) .provider((String) jwtToken.getClaims().get("provider")) .claims(jwtToken.getClaims()) .createdAt(jwtToken.getIssuedAt()) .expiresAt(jwtToken.getExpiresAt()) .build(); } public static class AuthException extends RuntimeException { public AuthException(String message) { super(message); } public AuthException(String message, Throwable cause) { super(message, cause); } } } Session Management
// SessionService.java package com.example.traefikauth.service; import com.example.traefikauth.config.AuthConfig; import com.example.traefikauth.model.UserSession; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.time.Duration; import java.time.LocalDateTime; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; @Slf4j @Service public class SessionService { private final RedisTemplate<String, UserSession> redisTemplate; private final JwtTokenService jwtTokenService; private final AuthConfig authConfig; private static final String SESSION_PREFIX = "session:"; private static final String USER_SESSION_PREFIX = "user_sessions:"; public SessionService(RedisTemplate<String, UserSession> redisTemplate, JwtTokenService jwtTokenService, AuthConfig authConfig) { this.redisTemplate = redisTemplate; this.jwtTokenService = jwtTokenService; this.authConfig = authConfig; } /** * Create new user session */ public UserSession createSession(String userId, String email, String username, String provider, java.util.Map<String, Object> claims) { String sessionId = UUID.randomUUID().toString(); LocalDateTime now = LocalDateTime.now(); LocalDateTime expiresAt = now.plusSeconds(authConfig.getCookie().getMaxAge()); UserSession session = UserSession.builder() .sessionId(sessionId) .userId(userId) .email(email) .username(username) .provider(provider) .claims(claims) .createdAt(now) .expiresAt(expiresAt) .build(); // Store session in Redis String sessionKey = SESSION_PREFIX + sessionId; String userSessionsKey = USER_SESSION_PREFIX + userId; long ttl = Duration.between(now, expiresAt).getSeconds(); redisTemplate.opsForValue().set(sessionKey, session, ttl, TimeUnit.SECONDS); redisTemplate.opsForSet().add(userSessionsKey, sessionId); redisTemplate.expire(userSessionsKey, ttl, TimeUnit.SECONDS); log.debug("Created session for user: {}", userId); return session; } /** * Get session by ID */ public Optional<UserSession> getSession(String sessionId) { if (sessionId == null) { return Optional.empty(); } String sessionKey = SESSION_PREFIX + sessionId; UserSession session = redisTemplate.opsForValue().get(sessionKey); if (session != null && session.isValid()) { return Optional.of(session); } // Session expired or not found if (session != null) { deleteSession(sessionId); } return Optional.empty(); } /** * Validate session and return user info */ public Optional<UserSession> validateSession(String sessionId) { return getSession(sessionId).filter(UserSession::isValid); } /** * Delete session */ public void deleteSession(String sessionId) { if (sessionId == null) { return; } String sessionKey = SESSION_PREFIX + sessionId; UserSession session = redisTemplate.opsForValue().get(sessionKey); if (session != null) { String userSessionsKey = USER_SESSION_PREFIX + session.getUserId(); redisTemplate.opsForSet().remove(userSessionsKey, sessionId); } redisTemplate.delete(sessionKey); log.debug("Deleted session: {}", sessionId); } /** * Delete all sessions for user */ public void deleteUserSessions(String userId) { String userSessionsKey = USER_SESSION_PREFIX + userId; java.util.Set<String> sessionIds = redisTemplate.opsForSet().members(userSessionsKey); if (sessionIds != null) { sessionIds.forEach(this::deleteSession); } redisTemplate.delete(userSessionsKey); log.debug("Deleted all sessions for user: {}", userId); } /** * Clean up expired sessions */ public void cleanupExpiredSessions() { // Redis TTL handles expiration automatically log.debug("Session cleanup completed"); } /** * Refresh session TTL */ public void refreshSession(String sessionId) { Optional<UserSession> sessionOpt = getSession(sessionId); if (sessionOpt.isPresent()) { UserSession session = sessionOpt.get(); LocalDateTime newExpiresAt = LocalDateTime.now() .plusSeconds(authConfig.getCookie().getMaxAge()); session.setExpiresAt(newExpiresAt); String sessionKey = SESSION_PREFIX + sessionId; long ttl = Duration.between(LocalDateTime.now(), newExpiresAt).getSeconds(); redisTemplate.opsForValue().set(sessionKey, session, ttl, TimeUnit.SECONDS); log.debug("Refreshed session: {}", sessionId); } } } Authentication Providers
1. OIDC Provider
// OidcAuthProvider.java package com.example.traefikauth.provider; import com.example.traefikauth.config.AuthConfig; import com.example.traefikauth.model.AuthResponse; import com.example.traefikauth.model.UserSession; import com.example.traefikauth.service.SessionService; import lombok.extern.slf4j.Slf4j; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.util.Map; @Slf4j @Component public class OidcAuthProvider { private final AuthConfig authConfig; private final SessionService sessionService; private final RestTemplate restTemplate; public OidcAuthProvider(AuthConfig authConfig, SessionService sessionService) { this.authConfig = authConfig; this.sessionService = sessionService; this.restTemplate = new RestTemplate(); } /** * Generate OIDC authorization URL */ public String getAuthorizationUrl(String redirectUri, String state) { AuthConfig.ProviderConfig.OidcConfig oidcConfig = authConfig.getProviders().getOidc(); return UriComponentsBuilder.fromHttpUrl(oidcConfig.getAuthorizationEndpoint()) .queryParam("client_id", oidcConfig.getClientId()) .queryParam("response_type", "code") .queryParam("scope", oidcConfig.getScopes()) .queryParam("redirect_uri", redirectUri) .queryParam("state", state) .queryParam("nonce", System.currentTimeMillis()) .build() .toUriString(); } /** * Exchange authorization code for tokens */ public Map<String, Object> exchangeCodeForTokens(String code, String redirectUri) { AuthConfig.ProviderConfig.OidcConfig oidcConfig = authConfig.getProviders().getOidc(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); String body = UriComponentsBuilder.newInstance() .queryParam("grant_type", "authorization_code") .queryParam("client_id", oidcConfig.getClientId()) .queryParam("client_secret", oidcConfig.getClientSecret()) .queryParam("code", code) .queryParam("redirect_uri", redirectUri) .build() .getQuery(); HttpEntity<String> request = new HttpEntity<>(body, headers); ResponseEntity<Map> response = restTemplate.exchange( oidcConfig.getTokenEndpoint(), HttpMethod.POST, request, Map.class ); if (!response.getStatusCode().is2xxSuccess()) { throw new RuntimeException("Failed to exchange code for tokens: " + response.getStatusCode()); } return response.getBody(); } /** * Get user info from OIDC provider */ public Map<String, Object> getUserInfo(String accessToken) { AuthConfig.ProviderConfig.OidcConfig oidcConfig = authConfig.getProviders().getOidc(); HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); HttpEntity<Void> request = new HttpEntity<>(headers); ResponseEntity<Map> response = restTemplate.exchange( oidcConfig.getUserInfoEndpoint(), HttpMethod.GET, request, Map.class ); if (!response.getStatusCode().is2xxSuccess()) { throw new RuntimeException("Failed to get user info: " + response.getStatusCode()); } return response.getBody(); } /** * Authenticate user with OIDC */ public AuthResponse authenticate(String code, String redirectUri, String state) { try { // Exchange code for tokens Map<String, Object> tokenResponse = exchangeCodeForTokens(code, redirectUri); String accessToken = (String) tokenResponse.get("access_token"); // Get user info Map<String, Object> userInfo = getUserInfo(accessToken); String userId = (String) userInfo.get("sub"); String email = (String) userInfo.get("email"); String username = (String) userInfo.get("preferred_username"); String name = (String) userInfo.get("name"); if (email == null) { email = (String) userInfo.get("upn"); // Fallback for Azure AD } if (username == null) { username = email != null ? email.split("@")[0] : userId; } // Create user session UserSession session = sessionService.createSession( userId, email, username, "oidc", userInfo ); log.info("OIDC authentication successful for user: {}", email); return AuthResponse.success(username, email); } catch (Exception e) { log.error("OIDC authentication failed", e); return AuthResponse.error("OIDC authentication failed: " + e.getMessage(), 401); } } public boolean isEnabled() { return authConfig.getProviders().getOidc().isEnabled(); } } 2. Basic Auth Provider
// BasicAuthProvider.java package com.example.traefikauth.provider; import com.example.traefikauth.config.AuthConfig; import com.example.traefikauth.model.AuthResponse; import com.example.traefikauth.model.UserSession; import com.example.traefikauth.service.SessionService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.util.Base64; import java.util.Optional; @Slf4j @Component public class BasicAuthProvider { private final AuthConfig authConfig; private final SessionService sessionService; public BasicAuthProvider(AuthConfig authConfig, SessionService sessionService) { this.authConfig = authConfig; this.sessionService = sessionService; } /** * Authenticate user with Basic Auth */ public AuthResponse authenticate(String authorizationHeader) { if (authorizationHeader == null || !authorizationHeader.startsWith("Basic ")) { return AuthResponse.error("Missing or invalid Authorization header", 401); } try { String base64Credentials = authorizationHeader.substring(6); byte[] credDecoded = Base64.getDecoder().decode(base64Credentials); String credentials = new String(credDecoded); final String[] values = credentials.split(":", 2); if (values.length != 2) { return AuthResponse.error("Invalid credentials format", 401); } String username = values[0]; String password = values[1]; // Validate against configured users Optional<AuthConfig.ProviderConfig.BasicAuthConfig.BasicUser> userOpt = authConfig.getProviders().getBasic().getUsers().stream() .filter(user -> user.getUsername().equals(username) && user.getPassword().equals(password)) .findFirst(); if (userOpt.isEmpty()) { log.warn("Basic auth failed for user: {}", username); return AuthResponse.error("Invalid credentials", 401); } // Create user session UserSession session = sessionService.createSession( username, username + "@local", username, "basic", null ); log.info("Basic authentication successful for user: {}", username); return AuthResponse.success(username, username + "@local"); } catch (Exception e) { log.error("Basic authentication failed", e); return AuthResponse.error("Basic authentication failed", 401); } } public boolean isEnabled() { return authConfig.getProviders().getBasic().isEnabled(); } } Cookie Management
// CookieService.java package com.example.traefikauth.service; import com.example.traefikauth.config.AuthConfig; import com.example.traefikauth.model.UserSession; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @Slf4j @Service public class CookieService { private final AuthConfig authConfig; private final JwtTokenService jwtTokenService; public CookieService(AuthConfig authConfig, JwtTokenService jwtTokenService) { this.authConfig = authConfig; this.jwtTokenService = jwtTokenService; } /** * Create authentication cookie */ public String createAuthCookie(UserSession session) { AuthConfig.CookieConfig cookieConfig = authConfig.getCookie(); // Generate JWT token for the session var jwtToken = jwtTokenService.generateToken(session); StringBuilder cookie = new StringBuilder(); cookie.append(cookieConfig.getName()).append("="); cookie.append(URLEncoder.encode(jwtToken.getToken(), StandardCharsets.UTF_8)); // Cookie attributes if (cookieConfig.getDomain() != null && !cookieConfig.getDomain().isEmpty()) { cookie.append("; Domain=").append(cookieConfig.getDomain()); } cookie.append("; Path=/"); cookie.append("; Max-Age=").append(cookieConfig.getMaxAge()); if (cookieConfig.isHttpOnly()) { cookie.append("; HttpOnly"); } if (cookieConfig.isSecure()) { cookie.append("; Secure"); } if (cookieConfig.getSameSite() != null && !cookieConfig.getSameSite().isEmpty()) { cookie.append("; SameSite=").append(cookieConfig.getSameSite()); } // Add expires attribute for older browsers ZonedDateTime expires = ZonedDateTime.now().plusSeconds(cookieConfig.getMaxAge()); String expiresStr = expires.format(DateTimeFormatter.RFC_1123_DATE_TIME); cookie.append("; Expires=").append(expiresStr); return cookie.toString(); } /** * Create logout cookie (expired) */ public String createLogoutCookie() { AuthConfig.CookieConfig cookieConfig = authConfig.getCookie(); StringBuilder cookie = new StringBuilder(); cookie.append(cookieConfig.getName()).append("="); cookie.append("deleted"); if (cookieConfig.getDomain() != null && !cookieConfig.getDomain().isEmpty()) { cookie.append("; Domain=").append(cookieConfig.getDomain()); } cookie.append("; Path=/"); cookie.append("; Max-Age=0"); if (cookieConfig.isHttpOnly()) { cookie.append("; HttpOnly"); } if (cookieConfig.isSecure()) { cookie.append("; Secure"); } // Set expired date ZonedDateTime expires = ZonedDateTime.now().minusDays(1); String expiresStr = expires.format(DateTimeFormatter.RFC_1123_DATE_TIME); cookie.append("; Expires=").append(expiresStr); return cookie.toString(); } /** * Extract session from cookie */ public UserSession getSessionFromCookie(String cookieHeader) { if (cookieHeader == null || cookieHeader.isEmpty()) { return null; } String cookieName = authConfig.getCookie().getName(); String[] cookies = cookieHeader.split(";"); for (String cookie : cookies) { String[] parts = cookie.trim().split("=", 2); if (parts.length == 2 && parts[0].equals(cookieName)) { try { String token = parts[1]; return jwtTokenService.extractSession(token); } catch (Exception e) { log.debug("Invalid auth cookie: {}", e.getMessage()); return null; } } } return null; } /** * Validate redirect URL against allowed domains */ public boolean isValidRedirectUrl(String redirectUrl) { if (redirectUrl == null || redirectUrl.isEmpty()) { return false; } try { java.net.URL url = new java.net.URL(redirectUrl); String host = url.getHost(); return authConfig.getAllowedDomains().stream() .anyMatch(domain -> host.equals(domain) || host.endsWith("." + domain)); } catch (Exception e) { log.warn("Invalid redirect URL: {}", redirectUrl); return false; } } } Main Authentication Service
```java
// TraefikAuthService.java
package com.example.traefikauth.service;
import com.example.traefikauth.model.AuthResponse;
import com.example.traefikauth.model.UserSession;
import com.example.traefikauth.provider.BasicAuthProvider;
import com.example.traefikauth.provider.OidcAuthProvider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Slf4j
@Service
public class TraefikAuthService {
private final SessionService sessionService;
private final CookieService cookieService;
private final OidcAuthProvider oidcAuthProvider;
private final BasicAuthProvider basicAuthProvider;
public TraefikAuthService(SessionService sessionService, CookieService cookieService, OidcAuthProvider oidcAuthProvider, BasicAuthProvider basicAuthProvider) { this.sessionService = sessionService; this.cookieService = cookieService; this.oidcAuthProvider = oidcAuthProvider; this.basicAuthProvider = basicAuthProvider; } /** * Main authentication method called by Traefik */ public AuthResponse authenticateRequest(HttpServletRequest request) { String path = request.getRequestURI(); String method = request.getMethod(); log.debug("Auth request: {} {}", method, path); // Check if path is whitelisted if (isWhitelisted(path)) { log.debug("Whitelisted path: {}", path); return AuthResponse.success("anonymous", "anonymous@local"); } // Extract session from cookie String cookieHeader = request.getHeader("Cookie"); Optional<UserSession> sessionOpt = Optional.ofNullable( cookieService.getSessionFromCookie(cookieHeader) ).flatMap(sessionService::validateSession); if (sessionOpt.isPresent()) { // Valid session found UserSession session = sessionOpt.get(); sessionService.refreshSession(session.getSessionId()); log.debug("Valid session for user: {}", session.getEmail()); return AuthResponse.success(session.getUsername(), session.getEmail()); } // No valid session - check for OIDC callback if (isOidcCallback(request)) { return handleOidcCallback(request); } // Check for Basic Auth if (basicAuthProvider.isEnabled()) { String authHeader = request.getHeader("Authorization"); if (authHeader != null && authHeader.startsWith("Basic ")) { return handleBasicAuth(authHeader); } } // No authentication found - redirect to login return redirectToLogin(request); } /** * Handle OIDC callback */ private AuthResponse handleOidcCallback(HttpServletRequest request) { String code = request.getParameter("code"); String state = request.getParameter("state"); String redirectUri = getCurrentUrl(request); if (code == null) { return AuthResponse.error("Missing authorization code", 400); } AuthResponse authResponse = oidcAuthProvider.authenticate(code, redirectUri, state); if (authResponse.isAuthenticated()) { // Set auth cookie String sessionId = "oidc-" + System.currentTimeMillis(); // In real implementation, get from session UserSession session = UserSession.builder() .sessionId(sessionId) .userId(authResponse.getUser()) .email(authResponse.getEmail()) .username(authResponse.getUser()) .provider("oidc") .build(); String authCookie = cookieService.createAuthCookie(session); authResponse.setSetCookie(authCookie); } return authResponse; } /** * Handle Basic Auth */ private AuthResponse handleBasicAuth(String authorizationHeader) { AuthResponse authResponse = basicAuthProvider.authenticate(authorizationHeader); if (authResponse.isAuthenticated()) { // Set auth cookie String sessionId = "basic-" + System.currentTimeMillis(); UserSession session = UserSession.builder() .sessionId(sessionId) .userId(authResponse.getUser()) .email(authResponse.getEmail()) .username(authResponse.getUser()) .provider("basic") .build(); String authCookie = cookieService.createAuthCookie(session); authResponse.setSetCookie(authCookie); } return authResponse; } /** * Redirect to OIDC login */ private AuthResponse redirectToLogin(HttpServletRequest request) { if (oidcAuthProvider.isEnabled()) { String redirectUri = getCurrentUrl(request); String state = generateState(); String authUrl = oidcAuthProvider.getAuthorizationUrl(redirectUri, state); log.debug("Redirecting to OIDC provider: {}", authUrl); return AuthResponse.redirect(authUrl); } if (basicAuthProvider.isEnabled()) { return AuthResponse.error("Authentication required", 401); } return AuthResponse.error("No authentication provider configured", 500); } private boolean isWhitelisted(String path) { // Implement whitelist check based on configuration return path.equals("/health") || path.equals("/metrics"); } private boolean isOidcCallback(HttpServletRequest request) { String path = request.getRequestURI(); return path.equals("/auth/callback") && request.getParameter("code") != null; } private String getCurrentUrl(HttpServletRequest request) { String scheme = request.getScheme(); String host = request.getServerName(); int port = request.getServerPort(); String path = request.getRequestURI(); String url = scheme + "://"