Skip to content

fix: enable PKCE on the OIDC client and surface IdP errors gracefully#8

Merged
dosaki merged 1 commit into
mainfrom
fix/oidc-pkce-and-error-handling
May 10, 2026
Merged

fix: enable PKCE on the OIDC client and surface IdP errors gracefully#8
dosaki merged 1 commit into
mainfrom
fix/oidc-pkce-and-error-handling

Conversation

@dosaki

@dosaki dosaki commented May 10, 2026

Copy link
Copy Markdown
Member

What broke

After PR #7 fixed the redirect_uri scheme, login made it past Keycloak's authorize step but 500'd on the callback:

```
authlib.integrations.base_client.errors.OAuthError: invalid_request: Missing parameter: code_challenge_method
GET /auth/callback?error=invalid_request&error_description=Missing+parameter%3A+code_challenge_method&state=...
```

Root cause

The Keycloak client is configured to require PKCE (S256). Authlib registers OIDC clients with PKCE off by default for backwards compatibility — server metadata says nothing about whether PKCE is required, so the SDK doesn't auto-enable it. Result: authorize redirect doesn't include `code_challenge` / `code_challenge_method`, Keycloak rejects the request, and the user lands on `/auth/callback?error=invalid_request` (with no authorization code). Authlib's `authorize_access_token()` then raises `OAuthError`, which we weren't catching, so Flask returned a 500.

Fix

Primary — opt into PKCE on the Authlib client (app/init.py):

```python
client_kwargs={
"scope": "openid email profile",
"code_challenge_method": "S256",
},
```

Authlib then generates the `code_verifier` per-request, hashes it (SHA-256), sends `code_challenge` + `code_challenge_method=S256` on the authorize redirect, and replays the verifier on the token exchange. All transparent — verifier is stashed in the Flask session.

Secondary — graceful error handling on the callback (app/auth/routes.py). Future IdP-side rejections (PKCE issue, user denying consent, scope mismatch) now log the upstream error+description at WARNING and redirect to `/auth/login` instead of returning 500. Cause shows up in CloudWatch, user gets a retry rather than a stack trace.

Why this wasn't caught earlier

My original prompt to the keycloak-config session said "PKCE Code Challenge Method: S256 (Advanced settings)" — they correctly set the server-side requirement, but the client-side opt-in in Authlib needs to be enabled separately. The two halves have to agree, and "the IdP requires it" doesn't auto-trigger the SDK to send it. Worth remembering for future OIDC integrations: PKCE is opt-in on the relying party.

Deploy

App-only change, no Terraform:

```bash
aws ecs update-service \
--region eu-west-1 \
--cluster librarian-services \
--service librarian-services-api \
--force-new-deployment
```

Test plan

  • After deploy, login flow completes through to library index without 500.
  • Inspect the authorize URL on `/auth/login` — query string should include `code_challenge=<43-char base64url>&code_challenge_method=S256`.
  • Force an error (e.g. revoke the user's `library-admin` role mid-flight, or hit `/auth/callback?error=access_denied&error_description=test` directly) — app should log a WARNING and redirect to `/auth/login` rather than 500.

🤖 Generated with Claude Code

Login was 500ing on the Keycloak callback with `Missing parameter:
code_challenge_method`. The Keycloak client requires PKCE (S256), but
Authlib doesn't auto-enable PKCE from server metadata for backwards
compatibility — it has to be opted into per-client. Adding
`code_challenge_method: "S256"` to client_kwargs makes Authlib generate
the code_verifier, hash it, and send code_challenge / code_challenge_method
on the authorize redirect, then replay the verifier on the token exchange.

Also: when Keycloak redirects to /auth/callback with ?error=... (no
authorization code), Authlib's authorize_access_token raises OAuthError.
That used to fall through to a generic Flask 500. Catch it explicitly,
log the upstream error/description, and bounce back to /auth/login —
the next IdP-side rejection will be diagnosable from CloudWatch instead
of opaque, and the user gets a fresh login attempt rather than an
internal-server-error page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 10, 2026 12:18

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables PKCE (S256) on the Authlib OIDC client to satisfy the Keycloak client’s PKCE requirement, and improves the /auth/callback handler so upstream IdP errors don’t surface as a 500.

Changes:

  • Opt in to PKCE by setting code_challenge_method="S256" on the registered Keycloak OAuth client.
  • Catch OAuthError in the OIDC callback, log the upstream error details at WARNING, and redirect users back to /auth/login instead of returning a 500.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
app/init.py Enables PKCE (S256) on the registered Keycloak Authlib client via client_kwargs.
app/auth/routes.py Adds OAuthError handling in the callback to log IdP rejection details and redirect to login gracefully.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@dosaki dosaki merged commit 5f618f8 into main May 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants