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 + "://"