Implement PKCE under an FF
What does this MR do?
Implements PKCE under an FF.
The verifier is created and stored on the origin site. We limit its validity to 10 minutes, same as Doorkeeper's authorization_code_expires_in default.
- Hop 1 (browser, origin to proxy): with the FF on, adds
code_verifierto the origin-domain session (alongsidestateetc.), which is hashed ascode_challengeand added to the proxy URL. - Hop 2 (browser, proxy to auth): when present, adds
code_challengeonto the authorize URL, and recordspkcein the proxy-domain session alongsideproxy_auth_domain(... which contains the origin host). If there's no challenge present, deletes anypkcefrom the session. - Hop 3 (browser, auth to proxy): GitLab redirects to the proxy redirect_uri with
codeandstate. - Hop 4 (browser, proxy to origin): when
pkceis present in the proxy- domain session, addspkce=1to the origin redirect and deletes it from the session. - Hop 5 (server-side POST, origin to auth): when
pkce=1was received and the origin-domain session hascode_verifier, addscode_verifieronto the token request body.
The FF is called FF_ENABLE_PKCE. It controls whether hop 1 generates a code_verifier for the origin-domain session, and whether auth state staleness is evaluated at all.
The pkce session variable and query parameter ensure that hop 5 only sends the verifier when hop 2 sent the challenge along. Without it, if we were mid-rollout or revert, and e.g. only hop 2 didn't have the PKCE code, hop 5 would see code_verifier in the origin-domain session and send it, resulting in a 503.
It is part of what gets us FF toggle safety, by having nothing but auth state refresh logic at hop 1 need to consult the FF; every hop after that works entirely with the data they receive and store, without consulting the FF again and thus raising the possibility of different FF states being read. It does mean, however, that an origin-domain session with a verifier stored will continue to use PKCE even with the FF disabled, until the session is reset.
Related:
Related to https://gitlab.com/gitlab-org/gitlab-pages/-/work_items/1183
Steps to verify locally
Here be dragons!
AI-generated text follows.
Verification steps
Setup
-
GDK with Pages access control enabled.
-
Build this branch: in
gdk/gitlab-pages,git fetch origin kiv/implement-pkce-under-an-ff && git checkout FETCH_HEAD && make. Regenerating GDK's Pages config checks out the pinned version again. -
A project with a deployed Pages site, with Pages visibility "Only project members". Below,
<site>is its URL and<pages>is the Pages host (e.g.pages.127.0.0.1.nip.io:3010). -
Toggle the FF in
gdk.yml, thengdk restart gitlab-pages:env: FF_ENABLE_PKCE: 'true' -
Use a new private window per test, with devtools' Network tab open and "Preserve log" on. Watch
gdk tail gitlab-pages | grep 'Authentication was successful'.
Opening <site> in a new private window stops at GitLab's sign-in page; that's the pause point below.
1. FF off: open <site>, sign in.
- No
code_challengeon<pages>/auth?…or/oauth/authorize?…; nopkce=1on<site>/auth?…. - Site loads; log has
"pkce":false.
2. FF on: open <site>, sign in.
<pages>/auth?…&code_challenge=X→/oauth/authorize?…&code_challenge=X&code_challenge_method=S256→ … →<site>/auth?code=…&pkce=1&state=….- Site loads; log has
"pkce":true. OauthAccessGrant.last.code_challengeisX.
3. FF turned off mid-flow: FF on, open <site>, stop at sign-in. Turn FF off, restart Pages, sign in.
pkce=1present; site loads;"pkce":true.
4. FF turned on mid-flow: FF off, open <site>, stop at sign-in. Turn FF on, restart Pages, sign in.
- No
pkce=1; site loads;"pkce":false.
5. GitLab enforces the challenge: FF on, open <site>, stop at sign-in. Copy the /oauth/authorize?… URL from the Network log, change code_challenge to any other 43-character value, load it, sign in.
<site>/auth?…returns 503; log hasfetching access token failed.
6. 10-minute TTL: also set FF_ENABLE_PROJECT_PREFIX_COOKIE_PATH: 'false'; by default the pre-auth cookie has Path=/auth, so every visit to <site> starts with a fresh state and verifier. The cookie itself expires 10 minutes after the last visit (auth-cookie-session-timeout), so it has to be kept alive while waiting. FF on, open <site>, stop at sign-in. Note state and code_challenge on <pages>/auth?…, and copy that URL.
- Open
<site>again at about 4 and 8 minutes: samestateandcode_challengeeach time. - At 11 minutes, open
<site>again: newstateandcode_challenge. - Load the copied URL, sign in:
<site>/auth?…returns 401; log hasAuthentication state did not match expected. - Open
<site>again: the newstateandcode_challengefrom before; site loads;"pkce":true.
Namespace in path
GDK has no setting for this, so edit the generated config by hand:
gitlab/config/gitlab.yml, underpages::namespace_in_path: true.gitlab-pages/gitlab-pages.conf: addnamespace-in-path=true, and changeauth-redirect-uritohttp://<pages>/projects/auth.- Add
http://<pages>/projects/authto the OAuth application's redirect URIs. gdk restart.
<site> is now http://<pages>/<namespace>/<project> (or http://<pages>/<unique-domain> with unique domains), as shown under Deploy > Pages. <namespace> below is its first path segment.
Tests 1–6 apply unchanged, except for the URLs:
<pages>/auth?…is<pages>/projects/auth?domain=http://<pages>/<namespace>&….<site>/auth?…is<pages>/<namespace>/auth?….
In test 6, the pre-auth cookie has Path=/<namespace>/auth by default, and Path=/<namespace> with FF_ENABLE_PROJECT_PREFIX_COOKIE_PATH: 'false'.
TODO
- Feature flag
- Added feature flag: [FF_ENABLE_PKCE] Use PKCE in the OAuth flow (#1208)
-
This feature does not require a feature flag
- I added the
Changelogtrailer to the commits that need to be included in the changelog (e.g.Changelog: added) - I added unit tests or they are not required
- I added acceptance tests or they are not required
- I added documentation (or it's not required)
- I followed code review guidelines
- I followed Go Style guidelines