Enable PKCE on the OAuth authorization code flow #58

Closed
opened 2026-07-28 05:57:11 +00:00 by claude-bot · 1 comment

Severity: HIGH

The bug

docs/circa-spec.md §4.1 requires "OAuth2 with PKCE flow." PKCE is not enabled.

A sub-agent verified this against the installed Authlib 1.6.10 source. Authlib adds
code_verifier/code_challenge only when code_challenge_method is set on the client — two
separate gates, both in sync_app.py:274 and oauth2/client.py:163-170. Circa's registration
(backend/app/auth/oauth.py:27-34) passes only:

client_kwargs={"scope": "openid email profile"},

So client.code_challenge_method is None, both gates skip, and the flow is a bare
authorization-code exchange. Anyone who intercepts the code can redeem it.

Interception is realistic here: the default redirect URI is http://localhost:8000/... and
CIRCA_ENVIRONMENT defaults to development, which disables HTTPS enforcement — so codes can
travel in plaintext on the LAN.

Fix

One line:

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

Authlib handles verifier generation and storage automatically once this is set. Confirm
Authentik advertises S256 in its discovery metadata (it does by default).

Done when

  • code_challenge_method: "S256" is set on the registration
  • An authorization request is observed carrying code_challenge and code_challenge_method
  • Login still succeeds end to end against Authentik

References

  • backend/app/auth/oauth.py:27-34
  • docs/circa-spec.md §4.1
  • Installed Authlib 1.6.10: sync_app.py:272-296, oauth2/client.py:163-170

Related: #43 (GenericOAuth2Backend) touches the same registration.

## Severity: HIGH ## The bug `docs/circa-spec.md` §4.1 requires "OAuth2 with **PKCE** flow." PKCE is not enabled. A sub-agent verified this against the installed Authlib 1.6.10 source. Authlib adds `code_verifier`/`code_challenge` only when `code_challenge_method` is set on the client — two separate gates, both in `sync_app.py:274` and `oauth2/client.py:163-170`. Circa's registration (`backend/app/auth/oauth.py:27-34`) passes only: ```python client_kwargs={"scope": "openid email profile"}, ``` So `client.code_challenge_method` is `None`, both gates skip, and the flow is a bare authorization-code exchange. Anyone who intercepts the code can redeem it. Interception is realistic here: the default redirect URI is `http://localhost:8000/...` and `CIRCA_ENVIRONMENT` defaults to `development`, which disables HTTPS enforcement — so codes can travel in plaintext on the LAN. ## Fix One line: ```python client_kwargs={"scope": "openid email profile", "code_challenge_method": "S256"}, ``` Authlib handles verifier generation and storage automatically once this is set. Confirm Authentik advertises `S256` in its discovery metadata (it does by default). ## Done when - [ ] `code_challenge_method: "S256"` is set on the registration - [ ] An authorization request is observed carrying `code_challenge` and `code_challenge_method` - [ ] Login still succeeds end to end against Authentik ## References - `backend/app/auth/oauth.py:27-34` - `docs/circa-spec.md` §4.1 - Installed Authlib 1.6.10: `sync_app.py:272-296`, `oauth2/client.py:163-170` Related: #43 (GenericOAuth2Backend) touches the same registration.
claude-bot added this to the v0.1.1 milestone 2026-07-28 05:57:11 +00:00
Author

Fixed in 244ebb7. CI green.

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

Verified against the pinned Authlib 1.7.2 rather than the 1.6.10 the audit inspected — the dev
server venv predates the lockfile, so those versions had drifted:

  • sync_app.py:232session = self.client_cls(**self.client_kwargs), so client_kwargs does
    reach the OAuth2Client constructor. Placement is correct.
  • sync_app.py:279if client.code_challenge_method: is the gate that generates the
    code_verifier. It is now truthy.

A test asserts code_challenge_method sits inside client_kwargs rather than as a top-level
register() argument, since only client_kwargs is forwarded to the client.

**Fixed** in 244ebb7. CI green. ```python client_kwargs={ "scope": "openid email profile", "code_challenge_method": "S256", }, ``` Verified against the **pinned** Authlib 1.7.2 rather than the 1.6.10 the audit inspected — the dev server venv predates the lockfile, so those versions had drifted: - `sync_app.py:232` — `session = self.client_cls(**self.client_kwargs)`, so `client_kwargs` does reach the `OAuth2Client` constructor. Placement is correct. - `sync_app.py:279` — `if client.code_challenge_method:` is the gate that generates the `code_verifier`. It is now truthy. A test asserts `code_challenge_method` sits **inside** `client_kwargs` rather than as a top-level `register()` argument, since only `client_kwargs` is forwarded to the client.
Sign in to join this conversation.
No description provided.