Skip to content

Commit e80128b

Browse files
authored
docs: update TP and add dev doc for account associations (#20221)
* docs: twine can handle the exchange Refs: https://twine.readthedocs.io/en/stable/changelog.html#twine-6-1-0-2025-01-17 Signed-off-by: Mike Fiedler <miketheman@gmail.com> * docs: account associations Signed-off-by: Mike Fiedler <miketheman@gmail.com> * Apply suggestion from @miketheman --------- Signed-off-by: Mike Fiedler <miketheman@gmail.com>
1 parent 812921b commit e80128b

3 files changed

Lines changed: 97 additions & 4 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Account Associations (OAuth backends)
2+
3+
Account associations let a PyPI user link their account
4+
to a third-party identity provider (GitHub, GitLab) for identity verification.
5+
The OAuth token is used only to confirm the external identity at link time;
6+
it is **not** stored.
7+
What gets stored is the provider name, the provider's stable numeric user ID,
8+
and the username.
9+
10+
This is distinct from Trusted Publishing (OIDC, in `warehouse/oidc/`),
11+
which authenticates CI/CD systems for uploads.
12+
Account associations authenticate a human linking their own profile.
13+
14+
## Architecture
15+
16+
Each provider is an implementation of `IOAuthProviderService` (`warehouse/accounts/oauth.py`).
17+
The concrete clients are:
18+
19+
| Provider | Real client | Null client (dev/test) | Flow |
20+
| -------- | ----------- | ---------------------- | ---- |
21+
| GitHub | `GitHubAppClient` | `NullGitHubOAuthClient` | GitHub App OAuth |
22+
| GitLab | `GitLabOAuthClient` | `NullGitLabOAuthClient` | OAuth 2.0, `read_user` scope |
23+
24+
Services are registered per-provider in `warehouse/accounts/__init__.py`
25+
via `register_service_factory(..., IOAuthProviderService, name="<provider>")`.
26+
The views live in `warehouse/manage/views/account_associations.py`
27+
and the `connect`/`callback` routes in `warehouse/routes.py`
28+
(`/manage/account/associations/<provider>/{connect,callback}`).
29+
30+
The provider's numeric `id` is the stable identifier for linking -
31+
usernames can change, IDs cannot.
32+
33+
## Configuration
34+
35+
Backends are selected by environment variable, parsed in `warehouse/config.py`
36+
with `maybe_set_compound` into `<provider>.oauth.backend` settings:
37+
38+
| Env var | Setting | Required? |
39+
| ------- | ------- | --------- |
40+
| `GITHUB_OAUTH_BACKEND` | `github.oauth.backend` | **Required** - registered unconditionally |
41+
| `GITLAB_OAUTH_BACKEND` | `gitlab.oauth.backend` | Optional - registered only when set; the views return 404 when unconfigured |
42+
43+
The value uses a compound format - the client class path
44+
followed by space-separated `key=value` kwargs:
45+
46+
```bash
47+
GITLAB_OAUTH_BACKEND=warehouse.accounts.oauth.GitLabOAuthClient client_id=<app-id> client_secret=<app-secret>
48+
```
49+
50+
The real clients read `<provider>.oauth.client_id` and
51+
`<provider>.oauth.client_secret` from settings in `create_service`.
52+
53+
### Local development
54+
55+
`dev/environment` already wires both providers to their Null clients,
56+
so the account-associations UI works locally without registering real OAuth apps:
57+
58+
```bash
59+
GITHUB_OAUTH_BACKEND=warehouse.accounts.oauth.NullGitHubOAuthClient
60+
GITLAB_OAUTH_BACKEND=warehouse.accounts.oauth.NullGitLabOAuthClient
61+
```
62+
63+
The Null clients simulate the OAuth round-trip and must never be used in
64+
production.
65+
66+
## Gotcha: missing `client_id`/`client_secret`
67+
68+
Setting a backend to the real client class **without** the `client_id` and
69+
`client_secret` kwargs is a footgun that passes startup and fails later:
70+
71+
```bash
72+
# Broken - no kwargs
73+
GITLAB_OAUTH_BACKEND=warehouse.accounts.oauth.GitLabOAuthClient
74+
```
75+
76+
`includeme` only checks that the backend setting is present, so the app boots fine.
77+
But `create_service` reads `settings["gitlab.oauth.client_id"]`,
78+
so the first time a user clicks "Connect", the request 500s with `KeyError: 'gitlab.oauth.client_id'`.
79+
Always pass both kwargs with the real client.
80+
This applies to every provider.
81+
82+
## Adding a new provider
83+
84+
Follow the `GitLabOAuthClient` pattern:
85+
86+
1. Add the client class (and a `Null*Client`) in
87+
`warehouse/accounts/oauth.py`, implementing `IOAuthProviderService`.
88+
2. Parse the backend env var in `warehouse/config.py` with `maybe_set_compound`.
89+
3. Register the service factory in `warehouse/accounts/__init__.py`
90+
(gate it on the setting being present if the provider is optional).
91+
4. Add `connect`/`callback` routes in `warehouse/routes.py` and views in
92+
`warehouse/manage/views/account_associations.py`.
93+
5. Update the account-associations UI in `warehouse/templates/manage/account.html`.
94+
6. Add tests in `tests/unit/accounts/test_oauth.py` and test config in `tests/conftest.py`.

‎docs/mkdocs-dev-docs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ nav:
102102
- Development Database: development/development-database.md
103103
- Cloud: development/cloud.md
104104
- Email: development/email.md
105+
- Account Associations: development/account-associations.md
105106
- Token Scanning: development/token-scanning.md
106107
- Documentation: development/documentation.md
107108
- Warehouse codebase: application.md

‎docs/user/trusted-publishers/security-model.md‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -272,14 +272,12 @@ own security model and considerations.
272272
strings like `v1.2.3`.
273273

274274
* **Limit the scope of your publishing job**: your publishing job should
275-
(ideally) have only three steps:
275+
(ideally) have only two steps:
276276

277277
1. Retrieve the publishable distribution files from **a separate
278278
build job**;
279279

280-
2. Exchange the OIDC token for a PyPI API token;
281-
282-
3. Publish the distributions using `twine` with the API token.
280+
2. Publish the distributions using `twine`, which handles the OIDC exchange.
283281

284282
By using a separate build job, you keep the number of steps that can
285283
access the OIDC token to a bare minimum. This prevents both accidental

0 commit comments

Comments
 (0)