OIDC Entegrasyonu
API Proxy istek hattına eklenecek olan groovy scripti:
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
import java.net.URLEncoder
import java.net.URLDecoder
import java.util.zip.GZIPOutputStream
import java.util.zip.GZIPInputStream
import java.io.ByteArrayOutputStream
import java.io.ByteArrayInputStream
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.SecureRandom
import java.time.Instant
import java.util.Base64
import java.util.UUID
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import org.apache.http.client.methods.HttpGet
import org.apache.http.client.methods.HttpPost
import org.apache.http.impl.client.HttpClients
import org.apache.http.entity.StringEntity
import org.apache.http.util.EntityUtils
// ################## OIDC Configuration ##################
def OIDC_CONFIG = [
clientId: "client_name",
clientSecret: "client_secret",
realm: "realm_name",
scope: "openid email",
discovery: "https://auth.keycloak.local/realms/realm_name/.well-known/openid-configuration",
authorizationEndpoint: "https://auth.keycloak.local/realms/realm_name/protocol/openid-connect/auth",
introspectionEndpoint: "https://auth.keycloak.local/realms/realm_name/protocol/openid-connect/token/introspect",
tokenEndpoint: "https://auth.keycloak.local/realms/realm_name/protocol/openid-connect/token",
redirectAfterLogoutUri: "https://auth.keycloak.local/realms/realm_name/protocol/openid-connect/logout",
postLogoutRedirectUri: "https://application.local/application_ui/",
accessTokenHeaderName: "Authorization",
accessTokenAsBearer: true,
idTokenHeaderName: "IdToken",
disableIdTokenHeader: false,
usePkce: true,
useNonce: true,
disableAccessTokenHeader: false,
ignoreRequestMethods: ["OPTIONS"],
ignoreRequestRegex: "static/media,static/js,static/css,static/html,*.json,*.ico,*.png,*.svg,*.js,*.woff2,*.css,*.html",
redirectAfterLogoutWithIdTokenHint: true,
logoutPath: "/logout",
redirectUri: "https://application.local/application_ui/",
// Session settings
sessionCookieName: "cookie_name",
sessionCookieSecure: true, // Use false for HTTP testing
sessionIdlingTimeout: 34560000,
sessionAbsoluteTimeout: 34560000,
// Encryption settings (for cookie)
encryptionKey: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", // 32-byte key for AES-256
encryptionIv: "a1b2c3d4e5f6g7h8", // 16-byte IV for AES
debugEnabled: false
]
// Handle OPTIONS requests immediately (pre-flight)
if (request_httpMethod == "OPTIONS") {
stopFlow(200,"CORS preflight handled")
return
}
// Stop flow according to business logic
def stopFlow = { Integer statusCode, String message ->
statusCodeToTargetAPI = statusCode
requestErrorMessageToTargetAPI = message
}
// Debug helper - consistent function
def debug = { String key, String value ->
if(OIDC_CONFIG.debugEnabled){
requestHeaderMapToTargetAPI.put(key, value)
}
}
def compressData = { String data ->
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream()
GZIPOutputStream gzipOut = new GZIPOutputStream(baos)
gzipOut.write(data.getBytes("UTF-8"))
gzipOut.close()
return Base64.getEncoder().encodeToString(baos.toByteArray())
} catch (Exception e) {
debug("compressError", "Error compressing data: ${e.message}")
throw e
}
}
def decompressData = { String compressedData ->
try {
byte[] decoded = Base64.getDecoder().decode(compressedData)
GZIPInputStream gzipIn = new GZIPInputStream(new ByteArrayInputStream(decoded))
ByteArrayOutputStream baos = new ByteArrayOutputStream()
byte[] buffer = new byte[1024]
int len
while ((len = gzipIn.read(buffer)) > 0) {
baos.write(buffer, 0, len)
}
gzipIn.close()
baos.close()
return new String(baos.toByteArray(), "UTF-8")
} catch (Exception e) {
debug("decompressError", "Error decompressing data: ${e.message}")
throw e
}
}
// URL encode utility
def encodeURIComponent = { String str ->
try {
return URLEncoder.encode(str, "UTF-8")
.replace("+", "%20")
.replace("%21", "!")
.replace("%27", "'")
.replace("%28", "(")
.replace("%29", ")")
.replace("%7E", "~")
} catch (Exception e) {
debug("encode-error", "Error encoding: " + e.getMessage())
return str
}
}
// Encrypt data
def encryptData = { String data ->
try {
def cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
def secretKey = new SecretKeySpec(OIDC_CONFIG.encryptionKey.getBytes(), "AES")
def ivSpec = new IvParameterSpec(OIDC_CONFIG.encryptionIv.getBytes())
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
def encryptedBytes = cipher.doFinal(data.getBytes())
return Base64.getEncoder().encodeToString(encryptedBytes)
} catch (Exception e) {
debug("encryptError", "Error encrypting data: ${e.message}")
throw e
}
}
// Decrypt data
def decryptData = { String encryptedData ->
try {
def cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
def secretKey = new SecretKeySpec(OIDC_CONFIG.encryptionKey.getBytes(), "AES")
def ivSpec = new IvParameterSpec(OIDC_CONFIG.encryptionIv.getBytes())
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
def decodedBytes = Base64.getDecoder().decode(encryptedData)
def decryptedBytes = cipher.doFinal(decodedBytes)
return new String(decryptedBytes)
} catch (Exception e) {
debug("decryptError", "Error decrypting data: ${e.message}")
throw e
}
}
// Create cookie and add to headers - single cookie approach
def setOidcCookie = { Map data, int maxAge ->
try {
// Convert to JSON and encrypt
def jsonData = JsonOutput.toJson(data)
// Compress the data
def compressedData = compressData(jsonData)
// Encrypt the compressed data
def encryptedData = encryptData(compressedData)
// URL encode the cookie value
def encodedValue = URLEncoder.encode(encryptedData, "UTF-8")
// Log the size of compressed data
debug("compression-stats", "Original size: ${jsonData.length()}, " +
"Compressed size: ${compressedData.length()}, " +
"Final size: ${encodedValue.length()}")
// Cookie template
def cookieValue = OIDC_CONFIG.sessionCookieName + "=" + encodedValue + "; " +
"Path=/; " +
"Max-Age=" + maxAge + "; " +
"HttpOnly; " +
"SameSite=Lax"
if (OIDC_CONFIG.sessionCookieSecure) {
cookieValue += "; Secure"
}
// Add cookie only to Set-Cookie header
customVariableMap.put("Set-Cookie", cookieValue)
// Add debug information
debug("cookie-set", "Compressed OIDC cookie set, length: " + encodedValue.length())
return true
} catch (Exception e) {
debug("cookie-set-error", "Error setting compressed OIDC cookie: " + e.getMessage())
return false
}
}
def getOidcCookie = {
try {
// Get Cookie header
def cookieHeader = requestHeaderMapFromClient.get("Cookie")
if (!cookieHeader) {
debug("cookie-read", "No cookies in request")
return null
}
// Parse cookies
def cookies = [:]
cookieHeader.split(";").each { cookie ->
def parts = cookie.trim().split("=", 2)
if (parts.length == 2) {
cookies[parts[0]] = URLDecoder.decode(parts[1], "UTF-8")
}
}
// Find OIDC cookie
if (!cookies.containsKey(OIDC_CONFIG.sessionCookieName)) {
debug("cookie-read", "OIDC cookie not found")
return null
}
// Decrypt
def encryptedData = cookies[OIDC_CONFIG.sessionCookieName]
def compressedData = decryptData(encryptedData)
// Decompress data
def jsonData = decompressData(compressedData)
// Convert to JSON
def sessionData = new JsonSlurper().parseText(jsonData)
debug("cookie-read", "Compressed OIDC cookie read successfully")
return sessionData
} catch (Exception e) {
debug("cookie-read-error", "Error reading compressed OIDC cookie: " + e.getMessage())
return null
}
}
// Function to remove cookie
def removeOidcCookie = {
try {
// Reset cookie and set Max-Age to 0
def cookieValue = OIDC_CONFIG.sessionCookieName + "=; " +
"Path=/; " +
"Max-Age=0; " +
"HttpOnly; " +
"SameSite=Lax"
if (OIDC_CONFIG.sessionCookieSecure) {
cookieValue += "; Secure"
}
// Add cookie to customVariableMap
customVariableMap.put("Set-Cookie", cookieValue)
debug("cookie-remove", "OIDC cookie removed")
return true
} catch (Exception e) {
debug("cookie-remove-error", "Error removing OIDC cookie: " + e.getMessage())
return false
}
}
// Get session data from cookie
def getSessionData = {
def sessionData = null
def cookieHeader = requestHeaderMapFromClient.get("Cookie")
debug("GetSessionData", "Cookie header: " + (cookieHeader ? "present" : "null"))
if (cookieHeader) {
def cookies = cookieHeader.split(";")
for (cookie in cookies) {
def parts = cookie.trim().split("=", 2)
if (parts.length == 2 && parts[0] == OIDC_CONFIG.sessionCookieName) {
try {
debug("GetSessionData", "Found cookie: " + parts[0] + ", length: " + parts[1].length())
// Decrypt cookie value
def decryptedData = decryptData(parts[1])
sessionData = new JsonSlurper().parseText(decryptedData)
debug("GetSessionData", "Successfully decrypted session data")
return sessionData
} catch (Exception e) {
debug("GetSessionData-error", e.getMessage())
}
break
}
}
}
// Alternative: try getAllCookies
def allCookies = getAllCookies()
if (allCookies.containsKey(OIDC_CONFIG.sessionCookieName)) {
try {
def cookieValue = allCookies.get(OIDC_CONFIG.sessionCookieName)
debug("GetSessionData-alt", "Found in getAllCookies, length: " + cookieValue.length())
def decryptedData = decryptData(cookieValue)
sessionData = new JsonSlurper().parseText(decryptedData)
debug("GetSessionData-alt", "Successfully decrypted alternative session data")
return sessionData
} catch (Exception e) {
debug("GetSessionData-alt-error", e.getMessage())
}
}
debug("GetSessionData", "No valid session found")
return null
}
// Validate token
def validateToken = { String token ->
if (!token) return false
try {
def introspectionEndpoint = OIDC_CONFIG.introspectionEndpoint
// Prepare introspection request parameters
def params = [
token: token,
client_id: OIDC_CONFIG.clientId,
client_secret: OIDC_CONFIG.clientSecret
]
// Build request body
StringBuilder bodyBuilder = new StringBuilder()
boolean first = true
params.each { key, value ->
if (!first) {
bodyBuilder.append("&")
}
bodyBuilder.append(encodeURIComponent(key.toString()))
bodyBuilder.append("=")
bodyBuilder.append(encodeURIComponent(value.toString()))
first = false
}
def requestBody = bodyBuilder.toString()
// Prepare HTTP client and request
def httpClient = HttpClients.createDefault()
def introspectRequest = new HttpPost(introspectionEndpoint)
introspectRequest.setHeader("Content-Type", "application/x-www-form-urlencoded")
introspectRequest.setEntity(new StringEntity(requestBody))
// Execute request
def response = httpClient.execute(introspectRequest)
def statusCode = response.getStatusLine().getStatusCode()
def responseBody = EntityUtils.toString(response.getEntity())
if (statusCode == 200) {
def introspectionResult = new JsonSlurper().parseText(responseBody)
return introspectionResult.active == true
}
} catch (Exception e) {
debug("tokenValidationError", "Error validating token: " + e.getMessage())
}
return false
}
// ################## Path Matching Methods ##################
// Method to check if a request should be ignored based on patterns from config
def shouldIgnoreRequest(String requestPath, String configPatternsString) {
// Get patterns from config and trim each one
def patterns = configPatternsString.split(",").collect { it.trim() }
// Check each pattern
for (pattern in patterns) {
if (matchesPattern(requestPath, pattern)) {
return true
}
}
return false
}
// Method to check if a path matches a specific pattern (wildcard or exact)
def matchesPattern(String path, String pattern) {
// File extension wildcard (*.ext)
if (pattern.startsWith("*.")) {
String extension = pattern.substring(2)
return path.endsWith("." + extension)
}
// Path wildcard (dir/*)
else if (pattern.endsWith("/*")) {
String prefix = pattern.substring(0, pattern.length() - 2)
return path.startsWith(prefix) || path.contains("/" + prefix + "/") || path.contains(prefix + "/")
}
// Exact pattern match
else {
return path.contains(pattern)
}
}
// ################## Main Process ##################
def path = request_pathInfo
def method = request_httpMethod
// Check code and state parameters
def code = requestUrlParamMapFromClient.get("code")
def state = requestUrlParamMapFromClient.get("state")
// ----------------- Callback Process -----------------
if (code && state) {
debug("Callback", "Callback path detected: " + path)
// Get login data from cookie
def sessionData = getOidcCookie()
// Return error if no session or not a login flow
if (!sessionData || sessionData.flow != "login") {
debug("Callback-error", "No valid login session found")
stopFlow(400,"No valid login session found")
return
}
// Compare state value
if (state != sessionData.state) {
debug("Callback-error", "State mismatch. Expected: ${sessionData.state}, Got: ${state}")
stopFlow(400,"Invalid state parameter")
return
}
// Prepare parameters for token exchange
def tokenParams = [
grant_type: "authorization_code",
code: code,
redirect_uri: OIDC_CONFIG.redirectUri,
client_id: OIDC_CONFIG.clientId,
client_secret: OIDC_CONFIG.clientSecret
]
// Add PKCE code_verifier
if (sessionData.code_verifier && OIDC_CONFIG.usePkce) {
tokenParams.code_verifier = sessionData.code_verifier
debug("Callback-pkce", "Added code_verifier to token request")
} else {
debug("Callback-error", "No code_verifier in session")
stopFlow(400,"PKCE code_verifier missing")
return
}
// Make HTTP request for token exchange
try {
// Create POST body
StringBuilder tokenBody = new StringBuilder()
boolean first = true
tokenParams.each { key, value ->
if (!first) tokenBody.append("&")
tokenBody.append(encodeURIComponent(key))
tokenBody.append("=")
tokenBody.append(encodeURIComponent(value))
first = false
}
// Retrieve certificate from environment and handle sslContext if needed
java.security.cert.X509Certificate cert= environment_certificateMap.get("wildcard.local");
// Create a KeyStore with the certificate
def keyStore = java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("cert-alias", cert);
// Create SSL context with the KeyStore
def sslContext = org.apache.http.ssl.SSLContexts.custom()
.loadTrustMaterial(keyStore, null)
.build();
// Create a custom SSL socket factory with the SSL context
def sslSocketFactory = new org.apache.http.conn.ssl.SSLConnectionSocketFactory( sslContext, null, null, org.apache.http.conn.ssl.NoopHostnameVerifier.INSTANCE);
// Register the SSL socket factory with connection manager
def connManager = new org.apache.http.impl.conn.PoolingHttpClientConnectionManager(
org.apache.http.config.RegistryBuilder.<org.apache.http.conn.socket.ConnectionSocketFactory>create()
.register("https", sslSocketFactory)
.register("http", new org.apache.http.conn.socket.PlainConnectionSocketFactory())
.build()
);
// Build the HTTP client with the custom connection manager
def httpClient = HttpClients.custom()
.setConnectionManager(connManager)
.build();
// Create and execute the token request
def tokenRequest = new HttpPost(OIDC_CONFIG.tokenEndpoint)
tokenRequest.setHeader("Content-Type", "application/x-www-form-urlencoded")
tokenRequest.setEntity(new StringEntity(tokenBody.toString()))
def response = httpClient.execute(tokenRequest)
def statusCode = response.getStatusLine().getStatusCode()
def responseBody = EntityUtils.toString(response.getEntity())
debug("tokenResponse responseBody", responseBody)
// Process token response
if (statusCode == 200) {
def tokenResponse = new JsonSlurper().parseText(responseBody)
debug("tokenResponse id_token", tokenResponse.id_token)
// Extract user information from token
def idToken = tokenResponse.id_token
def tokenExpiration = null
def username = null
def email = null
def userId = null
if (idToken) {
try {
def parts = idToken.split("\\.")
if (parts.length >= 2) {
def payload = parts[1]
while (payload.length() % 4 != 0) {
payload += "="
}
def decodedPayload = new String(Base64.getUrlDecoder().decode(payload))
def claims = new JsonSlurper().parseText(decodedPayload)
tokenExpiration = claims.exp
username = claims.preferred_username ?: claims.name
email = claims.email
userId = claims.sub
}
} catch (Exception e) {
debug("token-parse-error", "Error parsing ID token: " + e.getMessage())
}
}
// New session data - does not include token if not requested
def newSessionData = [
flow: "authenticated",
access_token: tokenResponse.access_token,
refresh_token: tokenResponse.refresh_token,
created_at: Instant.now().epochSecond,
expires_at: tokenExpiration ?: (Instant.now().epochSecond + tokenResponse.expires_in),
user_info: [
username: username,
email: email,
id: userId
]
]
// Add ID token only if required
if (!OIDC_CONFIG.disableIdTokenHeader) {
newSessionData.id_token = tokenResponse.id_token
}
// Set long-term session cookie - get duration from OIDC_CONFIG
setOidcCookie(newSessionData, OIDC_CONFIG.sessionAbsoluteTimeout)
// Redirect to original URL
def redirectUrl = sessionData.original_url ?: OIDC_CONFIG.postLogoutRedirectUri
customVariableMap.put("Location", redirectUrl)
stopFlow(302,"Authentication successful")
return
} else {
debug("token-error", "Failed to exchange code for tokens: " + responseBody)
stopFlow(500,"Token exchange failed")
return
}
} catch (Exception e) {
debug("token-exception", "Error during token exchange: " + e.getMessage())
stopFlow(500,"Error during token exchange")
return
}
}
// ----------------- Logout Process -----------------
else if (path.contains(OIDC_CONFIG.logoutPath)) {
debug("Logout", "Logout path detected: " + path)
// Read session cookie
def sessionData = getOidcCookie()
// Remove cookie
removeOidcCookie()
// Create Keycloak logout URL
def logoutUrl = OIDC_CONFIG.redirectAfterLogoutUri
// Add ID token as hint if available
if (sessionData && sessionData.id_token && OIDC_CONFIG.redirectAfterLogoutWithIdTokenHint ) {
logoutUrl += "?id_token_hint=" + encodeURIComponent(sessionData.id_token)
// Add post-logout redirect URL
if (OIDC_CONFIG.postLogoutRedirectUri) {
logoutUrl += "&post_logout_redirect_uri=" + encodeURIComponent(OIDC_CONFIG.postLogoutRedirectUri)
}
} else if (OIDC_CONFIG.postLogoutRedirectUri) {
// Add only redirect URL if no ID token
logoutUrl += "?post_logout_redirect_uri=" + encodeURIComponent(OIDC_CONFIG.postLogoutRedirectUri)
}
debug("logoutUrl",logoutUrl)
// Redirect to Keycloak logout page
customVariableMap.put("Location", logoutUrl)
stopFlow(302,"Logout successful")
return
}
// ----------------- Regular API Requests -----------------
else {
debug("ProtectedRequest", "Protected resource requested: " + path)
// Ignore path check
// Check if request method should be ignored
if (OIDC_CONFIG.ignoreRequestMethods.contains(request_httpMethod)) {
debug("IgnoreReason", "Request method " + request_httpMethod + " is ignored")
return
}
// Check if path should be ignored based on patterns
if (shouldIgnoreRequest(path, OIDC_CONFIG.ignoreRequestRegex)) {
debug("PathIgnored", "Request will proceed without authentication")
return
}
// Read session cookie
def sessionData = getOidcCookie()
// Redirect to login if no session
if (!sessionData || sessionData.flow != "authenticated" || !sessionData.access_token) {
// ----------------- Login Process -----------------
debug("Login", "Login Process started")
// Create state, code_verifier and nonce
state = UUID.randomUUID().toString()
// PKCE code verifier and challenge
def secureRandom = new SecureRandom()
byte[] bytes = new byte[32]
secureRandom.nextBytes(bytes)
def codeVerifier = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
def digest = MessageDigest.getInstance("SHA-256")
byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8))
def codeChallenge = Base64.getUrlEncoder().withoutPadding().encodeToString(hash)
// Create nonce
def nonce = OIDC_CONFIG.useNonce ? UUID.randomUUID().toString() : null
// Save original URL
def originalUrl = request_requestURI
if (request_queryString) {
originalUrl += "?" + request_queryString
}
if (originalUrl.trim() == "" || originalUrl == "/") {
originalUrl = "/"
}
// Store all data in a single cookie
sessionData = [
flow: "login", // Flow type
state: state, // CSRF protection
code_verifier: codeVerifier, // For PKCE
code_challenge: codeChallenge, // For debugging
nonce: nonce, // Replay protection
original_url: originalUrl, // Redirect after callback
created_at: Instant.now().epochSecond // Timestamp
]
// Set cookie - use duration from OIDC_CONFIG for login
setOidcCookie(sessionData, OIDC_CONFIG.sessionIdlingTimeout)
// Debug: Log state and code_verifier values
debug("state", state)
debug("code_verifier", codeVerifier.substring(0, 10) + "...")
// Create Keycloak auth URL
def authParams = [
client_id: OIDC_CONFIG.clientId,
response_type: "code",
redirect_uri: OIDC_CONFIG.redirectUri,
scope: OIDC_CONFIG.scope,
state: state
]
// Add PKCE
if (OIDC_CONFIG.usePkce) {
authParams.code_challenge = codeChallenge
authParams.code_challenge_method = "S256"
}
// Add nonce
if (nonce && OIDC_CONFIG.useNonce) {
authParams.nonce = nonce
}
// Create URL
StringBuilder urlBuilder = new StringBuilder(OIDC_CONFIG.authorizationEndpoint)
urlBuilder.append("?")
boolean first = true
authParams.each { k, v ->
if (!first) {
urlBuilder.append("&")
}
urlBuilder.append(encodeURIComponent(k.toString()))
urlBuilder.append("=")
urlBuilder.append(encodeURIComponent(v.toString()))
first = false
}
def authUrl = urlBuilder.toString()
// Debug: Log auth URL
debug("auth_url", authUrl)
// Set redirection
customVariableMap.put("Location", authUrl)
// Redirect with 302
stopFlow(302,"Redirecting to authentication provider")
return
}
// Add access token to request
if (!OIDC_CONFIG.disableAccessTokenHeader) {
def tokenPrefix = OIDC_CONFIG.accessTokenAsBearer ? "Bearer " : ""
requestHeaderMapToTargetAPI.put(OIDC_CONFIG.accessTokenHeaderName, tokenPrefix + sessionData.access_token)
}
// Add ID token to request (if disableIdTokenHeader is false)
if (!OIDC_CONFIG.disableIdTokenHeader && sessionData.id_token) {
requestHeaderMapToTargetAPI.put(OIDC_CONFIG.idTokenHeaderName, sessionData.id_token)
}
// Forward request to backend
return
}
API Proxy hata hattına eklenecek olan groovy scripti:
if(customVariableMap.get("Location")!=null ){
responseHeaderMapToClient.put("Location", customVariableMap.get("Location"))
statusCodeToClient=302;
}
customVariableMap.each { key, value ->
if (key.toLowerCase().contains("cookie")) {
responseHeaderMapToClient.put(key, value)
}
}
Gateway çözümleri ile OIDC (OpenID Connect) entegrasyonu yaparken dikkate alınması gereken bazı kritik hususlar bulunmaktadır.
Sorun 1: OIDC Parametrelerinin İletim Modu
Sorun: Varsayılan olarak, OIDC kimlik doğrulama işlemi parametreleri URL fragment'ları (#) kullanarak döndürür. Ancak fragment değerleri tarayıcıda kalır ve sunucuya gönderilmez. Bu durum, araya gateway çözümü eklendiğinde kimlik doğrulama hatalarına neden olur.
# Fragment kullanımı (ÇALIŞMAZ): https://example.com/callback#access_token=eyJ0...&token_type=bearer&...
Çözüm: OIDC yapılandırmanızda response_mode parametresini "query" olarak ayarlamanız gerekmektedir. Bu sayede parametreler fragment yerine query parametreleri (?) ile iletilir ve sunucuya başarıyla aktarılır.
# Query kullanımı (ÇALIŞIR): https://example.com/callback?access_token=eyJ0...&token_type=bearer&...
Yapılandırma:
- Keycloak için: İstemci ayarlarında, "Advanced Settings" (Gelişmiş Ayarlar) altında, "Response Mode" değerini "query" olarak ayarlayın.
- Diğer OIDC sağlayıcıları için: İlgili istemci yapılandırmasında "response_mode=query" parametresini ekleyin.
Sorun 2: Nginx Ingress Controller ile Header Boyutu Sınırlamaları
Sorun: OIDC'yi Nginx Ingress Controller ile kullanırken, kimlik doğrulama çerezleri ve başlıkları varsayılan tampon boyutu sınırlarını aşabilir. Bu durum, kimlik doğrulama işlemi sırasında 400 Bad Request
hatalarına veya kesik başlıklara neden olur.
Çözüm: Nginx Ingress Controller yapılandırmanızda tampon boyutu ayarlarını artırın:
nginx.ingress.kubernetes.io/proxy-buffer-size: "8k"
nginx.ingress.kubernetes.io/client-header-buffer-size: "8k"
nginx.ingress.kubernetes.io/large-client-header-buffers: "4 8k"
Bu ayarlar, Nginx Ingress Controller'ın OIDC kimlik doğrulama token'ları ve çerezleriyle yaygın olarak karşılaşılan daha büyük başlıkları düzgün bir şekilde işlemesini sağlar.