TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Two SSO Integration Methods: Agent (SDK) Inside the App vs. Gateway in Front of the App

Status: Draft (2026-10-26), reflects actual measurements. There are three sets of measurements. 1. We read the publicly readable metadata from Keycloak in the TT Lab cluster (/.well-known/openid-configuration, JWKS). Host and realm names and version numbers have been redacted. 2. We spun up a new Keycloak for practice (dev mode, a practice environment separate from production) on a laptop, generated a practice realm, client, and user with random values, and ran Authorization Code + PKCE using only Python standard libraries. We did not log in with a real user account. 3. We sent a JWT created with a practice key to a local gateway (agentgateway) for validation. The private part of the practice key was not included in the article. Specifications were verified by opening the original documents on the same day. Writing rule: Measurement data is the baseline; unmeasured items are marked as [Unmeasured], unverified items as [Not Verified], and guesses as [Estimated].

Table of Contents

1. Passes in One Place, Verification Where?

Think of a company building. Passes are issued at a single security office. This is the IdP(Identity Provider, e.g., Keycloak). There are two ways to verify a pass.

Both methods use the same protocol (OIDC) . The only difference is which process executes that protocol. Therefore, we will first look at the protocol (Sections 2–4), compare doing it inside the app (Section 5) versus in front of the app (Section 6), and then move on to logout (Section 7) and pitfalls (Section 8).

2. OAuth 2.0, OIDC, SAML: What Problems Do They Solve?

OAuth 2.0 OpenID Connect (OIDC) SAML 2.0
Problem Solved Delegated Authorization: Permission for an app to call APIs on behalf of the user Authentication: Informing the app "Who this person is" Authentication (and attribute transfer), mainly enterprise web SSO
Output Access token (free format) ID token (must be JWT) + OAuth tokens Signed XML Assertion
Location Foundation Layer built on top of OAuth 2.0 Separate protocol (2005)
Discovery — /.well-known/openid-configuration(Discovery) Metadata XML
Delivery Redirect + backchannel /token Same HTTP-Redirect (request) · HTTP-POST (response) bindings

It is important to note that OAuth 2.0 alone cannot handle "login." This is because an access token is meant for APIs, not a document proving to the app "who logged in." OIDC fills that gap with the ID token. OIDC Core §2 requires the ID token to include iss·sub·aud·exp·iat .

The shape of the SAML SP-initiated flow is similar. The SP (app) compresses AuthnRequest and sends it via redirect, the IdP logs the user in, and then passes a signed Response via HTML form POST to the app's ACS address (OASIS SAML 2.0 Technical Overview §5.1.2, §4.6). This article focuses on OIDC, which is mainly used in new integrations. We could not directly quote the normative documents of SAML (core and bindings PDFs) in the main text, so we verified them via the overview document.

3. Authorization Code + PKCE: Keeping Tokens Out of the Browser

Authorization Code + PKCE: 토큰은 브라우저를 지나지 않는다 브라우저 사용자 앱 (SDK) 또는 게이트웨이 IdP Keycloak 등 ① 보호된 페이지 요청 ② verifier 난수 → challenge S256(verifier). state·nonce 는 세션에 ③ 302 → IdP /auth challenge · state · nonce · redirect_uri ④ 로그인 화면 · 비밀번호 · MFA IdP 쿠키 = SSO 세션 ⑤ 302 → redirect_uri?code&state&iss code 는 한 번만, 짧게(초 단위) ⑥ /callback 도착 state 가 저장한 값과 같은가 ⑦ 뒤채널 POST /token code + verifier + 클라이언트 인증 ⑧ ID · 액세스 · 리프레시 토큰 ⑨ 서명·iss·aud·exp·nonce 확인 → 앱 세션 쿠키만 브라우저로 주황 화살표만 토큰이 오간다. 가로챈 code 만으로는 verifier 가 없어 교환이 안 된다. 실측(노트북 실습용 Keycloak): ③–⑥ 117ms, ⑦–⑧ 9ms.
Figure: The nine steps of Authorization Code + PKCE exchange among the browser, app, and IdP

The core of the flow is that only a short-lived code passes through the browser, while tokens are directly exchanged between the app and IdP via a backchannel. This is because the browser address bar, history, referrers, and browser extensions are all leak points. The old implicit flow gave tokens directly to the browser in the address #fragment , and OAuth Security BCP (RFC 9700, 2025-01) §2.1.2 now states that implicit should not be used. The same document also prohibits the password grant where the app receives the password and passes it to the IdP (§2.4).

Three Random Values: state, nonce, and PKCE

A single login request is accompanied by three random values, each serving a different purpose.

Random Value Where Checked What It Prevents
state Checked by app in callback CSRF where someone injects a forged callback into my browser
nonce Checked by app inside ID token Replay of an ID token from a different login
PKCE code_verifier /token Checked by IdP in Someone exchanging an intercepted code for tokens

The calculation for PKCE (RFC 7636) is a single line. The app creates a 43–128 character random value code_verifier (§4.1), and sends only its hash code_challenge = BASE64URL(SHA256(code_verifier)) in the login request (§4.2, S256). When exchanging the code for tokens, the original verifier must be provided, so anyone who merely stole the code can do nothing.

import base64, hashlib, secrets
b64u = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
verifier = b64u(secrets.token_bytes(32))                       # 43자
challenge = b64u(hashlib.sha256(verifier.encode()).digest())
print(b64u(hashlib.sha256(b"dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk").digest()))

The last line is the example verifier from Appendix B of RFC 7636. Running it locally resulted in E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM , which matched the appendix's expected value (2026-10-26). RFC 9700 §2.1.1 states that public clients (SPAs and mobile apps that cannot keep secrets) must use PKCE, and recommends it even for confidential server apps.

Measurement: A Round with Local Keycloak

We created a confidential client in the practice Keycloak, enforced PKCE with S256, and registered exactly one redirect URI. Then, mimicking the browser role with Python urllib , we faithfully stepped through steps ③–⑨ of the diagram above.

Step Result (2026-10-26, laptop, local IdP)
③–⑥ Login screen → Password submission → Callback 117–122ms. In callback code·state·session_state·iss Four values
Code length 110 characters
⑦–⑧ /token Exchange 200, 9ms. Received: access_token·id_token·refresh_token·expires_in and 8 other keys
Using the same code once more 400 invalid_grant "Code not valid"
With incorrect verifier 400 invalid_grant "PKCE verification failed: Code mismatch"
Omitting PKCE from login request IdP returns in callback invalid_request "Missing parameter: code_challenge_method"
Appending a / to the end of redirect_uri 400 error screen "Invalid parameter: redirect_uri", does not even return to callback

Having iss attached to the callback is thanks to the IdP having authorization_response_iss_parameter_supported enabled. This prevents mix-up attacks where an app using multiple IdPs gets confused about "which IdP sent this code."

One thing was stronger than expected: When trying to reuse the same code, even the session for the token already obtained with that code was terminated. Immediately after the code reuse attempt, introspecting the access token for that session returned active: false, and refreshing with the refresh token returned 400 "Session doesn't have required client." Since the code being used twice is a signal that someone may have intercepted the code, this behavior ensures that even previously issued tokens are no longer trusted. This is precisely the handling recommended by RFC 6749 §4.1.2. However, that access token was still valid for entities performing only signature verification(similar to the issue in Section 7).

4. Three Tokens and JWT Signature Verification

Token Recipient What It Is Used For Measured Lifespan (Practice Realm Settings) Measured Size
ID Token App (Client) "Who logged in." Once when creating the app session 300 seconds 1,145 bytes
Access Token API (Resource Server) API calls Authorization: Bearer 300 seconds 1,403 bytes
Refresh Token IdP only Getting a new access token 1,800 seconds (equals SSO session idle limit) —

Access and ID tokens together consume over a third of a single cookie's capacity (typically a 4KB limit). This is why gateways that place tokens into cookies must split them across multiple cookies or use a server-side session store.

JWT 세 조각과, 받는 쪽이 지켜야 할 검사 순서 헤더 페이로드(클레임) 서명 . . alg · typ · kid iss · sub · aud · exp · iat · nonce RS256: 개인키로 서명 base64url 로 감싼 JSON — 암호화가 아니라 그냥 보인다 1 alg 를 내가 정한 목록으로 고정 none · HS256 바꿔치기 거절 2 kid 로 JWKS 의 공개키 고르기 모르는 kid 면 JWKS 다시 받기(횟수 제한) 3 서명 검증 s^e mod n 이 SHA-256 과 맞나 4 iss 가 글자까지 같은가 끝의 / 하나도 다르면 실패 5 aud 에 내 이름이 있는가 다른 앱에 준 토큰 재사용 막기 6 exp · nbf (시계 오차 허용) 보통 60초 안팎 7 ID 토큰이면 nonce 로그인 요청과 묶였나 1–3 은 「누가 만들었나」, 4–7 은 「나에게, 지금, 이 로그인으로 온 것인가」.
Figure: The three parts of a JWT (header, payload, signature) and the seven verification steps the recipient must follow

JWT Has Three Pieces

JWT (RFC 7519) is a compact format of JWS (RFC 7515) formatted as BASE64URL(헤더).BASE64URL(페이로드).BASE64URL(서명) . Base64url is not encryption. Anyone can decode and read it, so secrets should never be placed in a token. The header of the measured token was {"alg":"RS256","typ":"JWT","kid":"…"} , and kid is the identifier used to select which key to verify with from the IdP's public key list (JWKS, RFC 7517).

RS256 verification ultimately boils down to a single exponentiation. Raising signature s to the public key (n, e) via s^e mod n should yield the PKCS#1 v1.5 format00 01 FF … FF 00 + DigestInfo + SHA-256(헤더.페이로드). We wrote it out directly using standard libraries for understanding (please use verified libraries in production code).

def rs256_ok(token, jwk):
    h64, p64, s64 = token.split(".")
    n = int.from_bytes(ub64(jwk["n"]), "big"); e = int.from_bytes(ub64(jwk["e"]), "big")
    k = (n.bit_length() + 7) // 8
    em = pow(int.from_bytes(ub64(s64), "big"), e, n).to_bytes(k, "big")
    di = bytes.fromhex("3031300d060960864801650304020105000420") + hashlib.sha256(f"{h64}.{p64}".encode()).digest()
    return em == b"\x00\x01" + b"\xff" * (k - 3 - len(di)) + b"\x00" + di

Measurement: The access, ID, and logout tokens issued by the practice IdP all passed this function using the 2048-bit RSA key from JWKS, and modifying even a single bit in the payload caused a failure.

A valid signature is not the end of the line

A signature only proves "created by the IdP." Whether "the token came to me, right now, with this login" must be verified separately through claims. OIDC Core §3.1.3.7 lists 13 steps for ID token validation, and the core is steps 4–7 in the diagram. iss text must match, aud must contain my client_id, exp must not have passed, and nonce must be the value I sent. JWT Best Current Practice (RFC 8725) states that callers should determine the list of algorithms that the library will accept (§3.1), and MUST aud validate alg: none (§3.9).

Two pitfalls were observed in the measured tokens.

  1. The aud of the Keycloak access token was account by default. This is not the name of my API. To properly validate aud on the API side, you must attach an audience mapper to the IdP so that the API name is included. If you skip validating aud on the API to save time, tokens issued by the same IdP to other apps could be used to call your API.
  2. The JWT access token standard (RFC 9068) specifies that the header typ should be at+jwt , but the measured token was JWT .(Claim typ was Bearer). If a resource server strictly checks typ per RFC 9068 §4, it will reject this token. You need to decide which side to align with before integration.

exp includes a clock skew tolerance (leeway). RFC 7519 §4.1.4 allows "a small leeway, typically no more than a few minutes." When the measured ID token was checked with a clock set to 1 second after expiration, it failed exp without leeway, but passed with a 60-second leeway. The default leeway for Spring Security resource servers is 60 seconds (official documentation), and the gateway in Section 6 also measured at about 60 seconds.

Cluster IdP Public Metadata

The Discovery document and JWKS were read from Keycloak in the TT Lab cluster (2026-10-26). Both are public information open without authentication, and the values below are the lists Keycloak provides by default for each realm.

Item Value
Discovery Document Key Count 57
Endpoints authorization, token, userinfo, end_session, jwks, introspection, revocation, device_authorization, PAR, CIBA, registration
grant_types_supported 10 types: authorization_code, refresh_token, client_credentials, device_code, token-exchange, jwt-bearer, uma-ticket, ciba, and implicit, password
code_challenge_methods_supported plain, S256
id_token_signing_alg_values_supported 13 types (RS, PS, ES, HS families and EdDSA)
Logout Supports both backchannel and frontchannel, including session (sid).
JWKS 2 keys: RS256 for signing (2048-bit), RSA-OAEP for encryption

The key takeaway here is that the Discovery document is a list of what the server "can do," not what is "allowed" for clients. Even if implicit, password, and plain are in this list, they can and should be disabled per client. The default stance is to enable only the standard flow (code) for each client while disabling implicit and direct access (password) flows.

5. Agent (SDK) Approach: What libraries inside the app do

같은 OIDC, 다른 자리: 앱 안에서 하나, 앱 앞에서 하나 에이전트(SDK) 방식 브라우저 앱 프로세스 OIDC 라이브러리 리디렉션·콜백·세션·갱신·로그아웃 업무 코드가 사용자·역할을 직접 본다 IdP 뒤채널 /token · 로그아웃 게이트웨이(프록시) 방식 브라우저 게이트웨이 oauth2-proxy: 로그인·쿠키 Envoy·agentgateway: JWT 서명·aud·exp 검사 통과하면 헤더를 붙여 넘김 앱 헤더만 읽음 IdP 뒤채널 SDK 게이트웨이 앱 코드 변경 필요(언어마다) 거의 없음 세분화된 권한 쉽다(코드 안) 헤더·경로 단위 앱이 직접 노출되면 그래도 막힌다 뚫린다(우회 경로 금지) 로그아웃 반영 뒤채널 받으면 즉시 쿠키·토큰 수명까지
Figure: Architecture of Agent (SDK) and Gateway approaches, and five comparisons

The OIDC library inside the app handles all of Sections 3–4 above. Listed out, it is longer than you might think.

  1. Reads the Discovery document at startup, fetches the JWKS, and caches it. Re-fetches (with rate limits) if an unknown kid arrives.
  2. If a request comes to a protected route without a session, it generates state, nonce, and verifier, stores them in server-side session, and redirects to the IdP.
  3. Verifies the state in the callback, obtains tokens using the code and verifier, and validates the ID token.
  4. Issues an app session cookie (HttpOnly, Secure, SameSite). Tokens are kept on the server.
  5. Refreshes the access token using the refresh token before it expires. Forces a re-login upon failure.
  6. Logout: Clears the app session and redirects to the IdP's end_session_endpoint (RP-Initiated Logout). Also opens an endpoint to receive backchannel logout requests sent by the IdP (Section 7).
  7. If providing an API (Resource Server), checks the signature, iss, aud, and exp of incoming Bearer tokens.

A local implementation simulating this process in Python was about 150 lines of code, and even that omitted session storage, key rotation, and concurrency. This is why you use a verified library instead of writing it yourself.

Available libraries and the retirement of Keycloak adapters

In the past, Keycloak provided language-specific adaptersdirectly. Most of these have now disappeared. Following the official blog, the timeline is as follows:

Date Event
2022-02 Announcement of deprecation for OIDC Java adapters (WildFly, Spring Boot, etc.) and Node.js adapter. Recommended alternatives include platform-standard OIDC support (WildFly native OIDC, Spring Security, Quarkus OIDC).
2023-03 Notice of support period extension. Alternative examples include Spring Security, Elytron OIDC, Quarkus OIDC.
2023-11, 23.0.0 Removal of OIDC adapter for WildFly and EAP.
2024-06, 25.0.0 Removal of Tomcat, Servlet Filter, Spring, andSpring Boot adapters.
2025-02 keycloak-js for browsers is maintained and separated into a distinct repository and release cycle.

The meaning of this trend is clear: OIDC has become standardized to the point where the IdP company no longer needs to maintain SDKs for every language. For Java, Spring Security is the representative choice.

# 로그인(oauth2Login) — 앱이 OIDC 클라이언트가 된다
spring.security.oauth2.client.provider.idp.issuer-uri: https://<IdP>/realms/<렐름>
spring.security.oauth2.client.registration.idp: { client-id: <앱>, client-secret: <비밀>, scope: [openid, profile] }
# 토큰 검사(oauth2ResourceServer) — API 가 Bearer 를 검증한다
spring.security.oauth2.resourceserver.jwt.issuer-uri: https://<IdP>/realms/<렐름>

issuer-uri It automatically fetches the JWKS address from the Discovery document using spring-boot-starter-security-oauth2-client·…-resource-server (on the first request), and the default clock leeway is 60 seconds (Spring Security documentation). In Spring Boot 4.x, the starter name changes to memoryand the old name is slated for removal. Authorized clients (tokens) of logged-in users are stored in OAuth2AuthorizedClientService by default. Only anonymous requests are stored in the session. If you scale out your app to multiple instances, this storage must be changed to a shared store.

Products like Argo CD, Grafana, Harbor, and Gitea have OIDC clients built-in , so you only need to enter the IdP address and client secret in the settings. This is also the agent approach; the only difference is whether you included the library or the product came with it.

6. Gateway Approach: Handling it at the entry point in front of apps

If you cannot modify the app (e.g., a dashboard built by someone else) or if there are multiple services in different languages, it is better to handle it once at the entry point. Gateways generally serve two functions:

Configurations combining both approaches are also common. A login proxy authenticates via PKCE, filters incoming users using the IdP's group claims, and attaches the token as a Authorization header, after which the subsequent service mesh re-validatesthe same JWT. In this setup, there is one rule: set the proxy's cookie refresh interval shorter than the IdP's access token lifespan, and keep the cookie expiration within the maximum length of the IdP's SSO session . This ensures the proxy obtains a new token before backend JWT validation encounters an expired one, and prevents users whose sessions have ended at the IdP from continuing to enter using only a cookie. In oauth2-proxy's official configuration table, cookie refresh (cookie_refresh) defaults to empty and is disabled if 0, while cookie expiration (cookie_expire) defaults to 168 hours (7 days). If you pass tokens downstream, you must configure both manually to match your IdP settings.

Measurement: Which tokens does the gateway filter out?

A JWT was created using a practice RSA key, and agentgateway with jwtAuth(strict, specifying iss and aud, JWKS file) enabled was run locally to send requests one by one (2026-10-26).

Sent Token Result Reason logged by gateway
Normal 200 —
No token 401 no bearer token found
Different aud app 401 InvalidAudience
Different iss 401 InvalidIssuer
One character changed in payload 401 InvalidSignature
alg: none, no signature 401 Header parsing failure (unknown alg)
alg: HS256, public key PEM used as HMAC secret (algorithm confusion) 401 InvalidAlgorithm
Unknown kid 401 unknown key
nbf after 2 minutes 401 ImmatureSignature
10 seconds past expiration 200 —

The last line is interesting. Changing the expiration to 10, 30, 55, 58, 62, 65, 90, and 120 seconds in the past showed that up to 58 seconds passed, and rejection started at 62 seconds. Similarly, nbf passed up to 55 seconds in the future, and rejection started at 65 seconds in the future. This means a clock skew tolerance of about 60 seconds is built in by default (not found in documentation, measured). This is why you should not design systems trusting expiration down to the exact second.

Costs were also measured. Calling the same backend directly, through an unauthenticated gateway, and through a JWT-validating gateway were alternated 400 times each (after 50 warm-up requests).

Path p50 p90 p99
Direct backend 0.60ms 0.73ms 1.19ms
Gateway (no auth) 2.51ms 3.05ms 4.82ms
Gateway (JWT validation) 2.59ms 3.17ms 5.21ms

Compared to the overhead of passing through an extra gateway layer (about 1.9ms, including laptop container network round-trips), RS256 validation itself was about 0.08ms . Validation cost is no reason to hesitate in using a gateway.

Pros and Cons of the Two Approaches

Agent (SDK) Gateway
App Code Must be modified (per language and framework) Almost unchanged
Authorization Decisions Granular within business logic (e.g., owner only) Up to path, method, and header levels
If an app is exposed via a different path Still blocked Exposed directly Alternative paths to the app (direct service exposure, port forwarding) must be blocked via network policies
Header Trust N/A The app must trust only headers attached by the gateway, and the gateway must strip out any incoming headers with the same name from the outside
Logout Propagation Immediately upon receiving a backchannel notification May persist until the cookie and token lifetimes expire
Operations Managing library versions per app Managing versions and configuration in one place, which creates a single point of failure instead

In reality, these approaches are mixed. For example, browser logins are handled by a gateway (or BFF), while granular API permissions are handled by a resource server library inside the app.

7. Logout and Session Synchronization

The reason SSO logout is difficult is because sessions are threefold. These include the IdP's SSO session (IdP cookie), the app's session (app cookie), and the already issued tokens held individually. Clearing just one leaves the others intact.

Method Direction Characteristics
RP-Initiated Logout App → IdP. end_session_endpoint id_token_hint Recommended, post_logout_redirect_uri must match the registered value exactly
Front-Channel Logout IdP loads each app's URL in an iframe on the logout page Prone to failure if the browser blocks third-party cookies
Back-Channel Logout IdP sends a direct POST to the app server Independent of the browser. logout_token(JWT) must not contain events and sub or sid, nonce must be absent

Measurement: Logged out, but the access token lives for 5 more minutes

We registered a back-channel logout URL in the practice IdP's client (a small HTTP server running on a laptop), logged in anew, and requested a logout using the refresh token.

Observation Value
Logout Request 204
Back-channel notification arrived 66ms later, application/x-www-form-urlencoded 's logout_token
logout_token header alg: RS256, typ: logout+jwt(spec recommendation)
logout_token claim iss·aud·iat·exp·jti·events·sid·sub·typ. nonce None. Lifetime 120 seconds
events {"http://schemas.openid.net/event/backchannel-logout": {}}
sid Matches the ID token's sid received during login → The app knows which session to clear
Refresh after logout 400 'Session not active'
Introspection after logout (RFC 7662) active: false
Signature and expiration check on the same access token after logout Passed, remaining lifetime 299 seconds

The last line is crucial. Because JWT access tokens are self-contained documents, APIs and gateways that only check signatures are unaware that the IdP has cleared the session. Even after logout, APIs can still be called for the duration of the token's lifetime (5 minutes here). Mitigation involves one of three options: shorten the access token lifetime, use introspection to query the IdP for important APIs (incurring a round-trip cost per request), or receive back-channel notifications and add that sid to a blocklist.

Refresh tokens also present a similar dilemma. In the practice realm's default setting (rotation disabled), reusing an already-used refresh token still returned 200 . When realm settings revokeRefreshToken was enabled (rotation), the old refresh token was rejected with 400 'Maximum allowed refresh token reuse exceeded', and immediately afterward, even the new refresh token was blocked with 'Session doesn't have required client'. This behavior treats reuse as a signal of theft and terminates the entire session. RFC 9700 §2.2.2 requires public clients' refresh tokens to be rotated or bound to the sender.

8. Common Pitfalls

① redirect_uri Mismatch. The IdP compares it strictly as a string against the registered URL (RFC 9700 §2.1, with the exception of native app localhost ports). In our measurements, a single trailing / triggered a 400 error page. The same error occurs if the app perceives its own address as http:// behind a proxy (when TLS is terminated upstream), or if port and path prefixes differ. Wildcard redirect URIs should be avoided because they provide attackers a way to intercept the authorization code.

② iss Mismatch. The token's iss and the app configuration's issuer must match down to the http/https, hostname, and trailing / . This frequently breaks in configurations where the IdP is called via an internal address while the browser uses a public address. Since Keycloak can construct iss using the requested hostname, it is safer to configure a fixed public hostname.

③ Clock Skew. If the clocks of the IdP, gateway, and app server are out of sync, a freshly received token may be considered 'not yet valid' (nbf, iat) or 'expired'. The measured default leeway for both the gateway and Spring was approximately 60 seconds. Sync NTP before increasing the leeway.

④ Storing Tokens in Browser Storage. If an SPA stores access and refresh tokens in localStorage , a single XSS vulnerability exposes everything. 'OAuth 2.0 for Browser-Based Applications', which became RFC 10017 (BCP 212) in August 2026, ranks patterns by security order from BFF (Backend for Frontend) to token-mediating backend to standalone browser, noting that standalone browser usage is vulnerable to all previously discussed attacks. A BFF acts as a server-side client with secrets, storing tokens on the server and issuing only HttpOnly session cookies to the browser. In other words, it is the server-side implementation of the SDK approach in Section 5.

⑤ Ignoring aud. As seen in the Keycloak default aud: account from Section 4, if a token intended for another target is accepted even though it is not meant for your API, tokens from other apps on the same IdP can be used to call your API.

⑥ Secure Cookies and Plaintext HTTP. The IdP's login cookie has Secure attached (measured). Browsers treat localhost·127.0.0.1 as a secure origin and send it, but do not send it over plaintext HTTP addresses on a LAN, causing infinite 'logged in, but back to the login screen' loops. During our measurements, a Python cookie store failed to send the cookie for this exact reason, resulting in a 400 error.

⑦ JWKS Key Rotation. When the IdP rotates keys, tokens with a new kid arrive. Validators that cache keys for too long (Envoy defaults to 10 minutes) may reject them during that window, so the IdP should publish new keys in advance and delay signing with them, while validators should re-fetch JWKS from unknown kid .

⑧ Gateway Bypassing and Header Spoofing. If the app trusts X-Auth-Request-User in a gateway setup, there must be no path to call the app directly from the outside by injecting those headers.

9. Which One to Choose

Scenario Recommended Approach
Placing login in front of a third-party web UI (dashboards, laptops) Gateway (oauth2-proxy)
Product already includes OIDC (Argo CD, Grafana, etc.) Built-in product feature (= SDK approach)
Custom web app built in-house, granular permissions Server-side SDK (BFF), Spring Security, or authentication libraries for each language
APIs called by service-to-service communication, CLIs, or agents using Bearer tokens Gateway JWT check + Resource server check inside the app (two layers)
Pure SPA, no server Use a BFF if possible. If difficult, use PKCE + short-lived tokens + rotation

10. Conclusion

Titi's Note: Even if a pass (token) has a stamp (signature), you still need to check separately whether it belongs to 'our ping-pong room' (aud) and is valid for 'today's date' (exp). Also, even after someone is asked to leave, the pass in their hand remains valid until its expiration date, so it's best to issue them with short lifespans.

References

Sign in to like

Comments

No comments yet.

Sign in to leave a comment