Traefik Forward Auth in Java: Complete Implementation Guide

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:

  1. Traefik intercepts requests to protected services
  2. Redirects to auth service for authentication
  3. Auth service validates credentials and sets cookies
  4. 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 + "://"

Leave a Reply

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


Macro Nepal Helper