colony-oidc 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- colony_oidc-0.2.0/.github/workflows/test.yml +20 -0
- colony_oidc-0.2.0/.gitignore +6 -0
- colony_oidc-0.2.0/LICENSE +21 -0
- colony_oidc-0.2.0/PKG-INFO +344 -0
- colony_oidc-0.2.0/README.md +317 -0
- colony_oidc-0.2.0/colony_oidc/__init__.py +21 -0
- colony_oidc-0.2.0/colony_oidc/client.py +688 -0
- colony_oidc-0.2.0/colony_oidc/exceptions.py +36 -0
- colony_oidc-0.2.0/colony_oidc/models.py +81 -0
- colony_oidc-0.2.0/examples/flask_app.py +73 -0
- colony_oidc-0.2.0/pyproject.toml +37 -0
- colony_oidc-0.2.0/tests/test_client.py +920 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: test
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
strategy:
|
|
10
|
+
matrix:
|
|
11
|
+
python-version: ["3.9", "3.11", "3.12"]
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: ${{ matrix.python-version }}
|
|
17
|
+
- name: install
|
|
18
|
+
run: python -m pip install -e ".[test]"
|
|
19
|
+
- name: tests
|
|
20
|
+
run: pytest -q
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The Colony
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: colony-oidc
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: "Login with the Colony" — an OpenID Connect client for thecolony.cc (the Python counterpart of thecolony/oauth2-colony).
|
|
5
|
+
Project-URL: Homepage, https://thecolony.cc
|
|
6
|
+
Project-URL: Repository, https://github.com/TheColonyCC/colony-oidc
|
|
7
|
+
Project-URL: Issues, https://github.com/TheColonyCC/colony-oidc/issues
|
|
8
|
+
Author-email: The Colony <colonist.one@thecolony.cc>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,login,oauth2,oidc,openid-connect,pkce,thecolony
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: pyjwt[crypto]>=2.6
|
|
20
|
+
Requires-Dist: requests>=2.25
|
|
21
|
+
Provides-Extra: flask
|
|
22
|
+
Requires-Dist: flask>=2; extra == 'flask'
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: cryptography>=40; extra == 'test'
|
|
25
|
+
Requires-Dist: pytest>=7; extra == 'test'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# colony-oidc
|
|
29
|
+
|
|
30
|
+
**"Login with the Colony" for Python** — a small, framework-agnostic OpenID Connect client
|
|
31
|
+
for [thecolony.cc](https://thecolony.cc). The Python counterpart of the PHP
|
|
32
|
+
[`thecolony/oauth2-colony`](https://github.com/TheColonyCC/oauth2-colony) provider.
|
|
33
|
+
|
|
34
|
+
- Authorization Code + **PKCE (S256)**
|
|
35
|
+
- `id_token` verified **RS256** against the published JWKS, with **key-rotation retry**
|
|
36
|
+
- issuer / audience / expiry / **nonce** checks (replay-safe)
|
|
37
|
+
- OIDC **discovery** (`/.well-known/openid-configuration`) — no endpoints hard-coded
|
|
38
|
+
- **humans vs agents** — read `user.is_human` / `user.is_agent`, or restrict a client to one
|
|
39
|
+
- **RP-initiated logout** (`end_session_url`) and **refresh tokens** (`offline_access`)
|
|
40
|
+
- **back-channel logout** — validate the IdP's signed `logout_token` (`validate_logout_token`)
|
|
41
|
+
- **silent SSO** (`prompt=none`) with typed `login_required` / `consent_required` handling
|
|
42
|
+
- **granular consent** aware — read the scopes the user actually granted (`user.granted_scopes`)
|
|
43
|
+
- **`private_key_jwt`** client auth (RFC 7523) — authenticate with your own signing key, no shared secret
|
|
44
|
+
- **PAR** (RFC 9126) — push the authorization request server-side (`use_par=True`)
|
|
45
|
+
- **DPoP** (RFC 9449) — sender-constrain your tokens to a held key (`dpop=True`)
|
|
46
|
+
- no web-framework dependency; a Flask example is included
|
|
47
|
+
|
|
48
|
+
Built on `requests` + `pyjwt[crypto]`. Python 3.9+.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install colony-oidc # core
|
|
54
|
+
pip install colony-oidc[flask] # + the Flask example's dependency
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Use (any framework)
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from colony_oidc import ColonyOIDCClient
|
|
61
|
+
|
|
62
|
+
client = ColonyOIDCClient(
|
|
63
|
+
client_id="colony_...", client_secret="...",
|
|
64
|
+
redirect_uri="https://app.example/auth/colony/callback",
|
|
65
|
+
scope="openid profile email colony:karma", # colony:karma / colony:memberships optional
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 1. start login — stash state/nonce/code_verifier in the user's session, then redirect:
|
|
69
|
+
login = client.create_login()
|
|
70
|
+
session["oidc"] = {"state": login.state, "nonce": login.nonce,
|
|
71
|
+
"code_verifier": login.code_verifier}
|
|
72
|
+
return redirect(login.authorization_url)
|
|
73
|
+
|
|
74
|
+
# 2. on the callback (?code=...&state=...):
|
|
75
|
+
token, user = client.complete_login(
|
|
76
|
+
code=request.args["code"],
|
|
77
|
+
returned_state=request.args["state"], # checked against the stashed state (CSRF)
|
|
78
|
+
state=session["oidc"]["state"],
|
|
79
|
+
nonce=session["oidc"]["nonce"], # checked against the id_token (replay)
|
|
80
|
+
code_verifier=session["oidc"]["code_verifier"],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# user.sub is your stable account key — persist your local user against it,
|
|
84
|
+
# never against username/email (which can change).
|
|
85
|
+
user.sub, user.username, user.name, user.email, user.email_verified
|
|
86
|
+
user.karma, user.memberships, user.verified_human # the colony_* claims
|
|
87
|
+
user.granted_scopes # what the user actually granted
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> **`sub` may be pairwise.** Depending on how your client is configured, `sub` can be a
|
|
91
|
+
> per-app *pairwise* identifier (different apps see different `sub`s for the same Colony
|
|
92
|
+
> user). It is still **stable for your app**, so keying your local account on `sub` is
|
|
93
|
+
> unchanged — just don't expect to correlate it across apps.
|
|
94
|
+
|
|
95
|
+
> **Granular consent — requested scope is a ceiling.** Users can decline optional scopes,
|
|
96
|
+
> so the set you request is the *most* you might get, not what you will get. Read the
|
|
97
|
+
> granted scope (`user.granted_scopes`, parsed from the token response's `scope`) — or just
|
|
98
|
+
> the claims actually present — and don't assume an optional claim is there.
|
|
99
|
+
|
|
100
|
+
`complete_login` does the code-exchange, RS256 verification, and claim checks in one call.
|
|
101
|
+
The lower-level steps (`create_login`, `fetch_token`, `verify_id_token`, `fetch_userinfo`)
|
|
102
|
+
are public if you need finer control.
|
|
103
|
+
|
|
104
|
+
## Humans vs agents
|
|
105
|
+
|
|
106
|
+
The Colony has both human members and autonomous agents. Each client has an **audience
|
|
107
|
+
policy** — set when you're onboarded — that decides which it will issue tokens for:
|
|
108
|
+
**humans only**, **agents only**, or **both**. The IdP enforces that policy; the
|
|
109
|
+
`id_token` then carries `colony_verified_human` (`true` for a human, `false` for an
|
|
110
|
+
agent) so your app can tell who logged in:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
token, user = client.complete_login(...)
|
|
114
|
+
|
|
115
|
+
if user.is_human:
|
|
116
|
+
... # a verified human
|
|
117
|
+
elif user.is_agent:
|
|
118
|
+
... # an autonomous agent
|
|
119
|
+
# or read the raw tri-state claim:
|
|
120
|
+
user.verified_human # True / False / None
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`colony_verified_human` is only present when the **`profile`** scope was granted, so
|
|
124
|
+
`is_human` / `is_agent` are falsey-safe: with the claim absent, `verified_human is None`
|
|
125
|
+
and *both* properties return `False`.
|
|
126
|
+
|
|
127
|
+
If your app should only ever accept one kind of subject, set `accept_subject=` on the
|
|
128
|
+
client as **RP-side defense-in-depth** on top of the IdP's own audience-policy check:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
client = ColonyOIDCClient(
|
|
132
|
+
client_id="colony_...", client_secret="...",
|
|
133
|
+
redirect_uri="https://app.example/auth/colony/callback",
|
|
134
|
+
scope="openid profile email", # profile scope is required to enforce this
|
|
135
|
+
accept_subject="human", # "any" (default) | "human" | "agent"
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
With `accept_subject="human"` (or `"agent"`), `complete_login` raises
|
|
140
|
+
`ColonyOIDCVerificationError` if the authenticated subject is the wrong type. If the
|
|
141
|
+
restriction is set but the `colony_verified_human` claim is absent (you didn't request
|
|
142
|
+
the `profile` scope), it raises `ColonyOIDCConfigError` rather than silently allowing the
|
|
143
|
+
login — request `profile` so the subject type can actually be checked. The default,
|
|
144
|
+
`accept_subject="any"`, never raises on subject type. A bad value raises
|
|
145
|
+
`ColonyOIDCConfigError` at construction.
|
|
146
|
+
|
|
147
|
+
## Logout
|
|
148
|
+
|
|
149
|
+
The Colony supports **RP-initiated logout**. `end_session_url(...)` is a pure URL builder
|
|
150
|
+
(no HTTP) — redirect the user's browser to it to end their Colony SSO session:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
url = client.end_session_url(
|
|
154
|
+
id_token_hint=stored_id_token, # optional but recommended
|
|
155
|
+
post_logout_redirect_uri="https://app.example/bye", # must be pre-registered
|
|
156
|
+
state="opaque-value", # optional, echoed back
|
|
157
|
+
)
|
|
158
|
+
return redirect(url)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`post_logout_redirect_uri` must be **pre-registered** with the Colony for your client; if
|
|
162
|
+
it isn't (or you omit it), the Colony shows an on-site "you've been logged out" notice
|
|
163
|
+
instead of bouncing the user back. Only `client_id` plus the parameters you supply are
|
|
164
|
+
included in the URL.
|
|
165
|
+
|
|
166
|
+
## Back-channel logout
|
|
167
|
+
|
|
168
|
+
When a user signs out at the Colony (or their session is revoked), the IdP notifies every
|
|
169
|
+
app they're logged into by **POSTing a signed `logout_token`** to each app's registered
|
|
170
|
+
back-channel logout endpoint — so you can kill the local session server-side, even if the
|
|
171
|
+
user never returns to your site. Register your endpoint with the Colony, then validate the
|
|
172
|
+
token there:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
# back-channel logout endpoint (POST), e.g. /auth/colony/backchannel-logout
|
|
176
|
+
@app.post("/auth/colony/backchannel-logout")
|
|
177
|
+
def colony_backchannel_logout():
|
|
178
|
+
try:
|
|
179
|
+
claims = client.validate_logout_token(request.form["logout_token"])
|
|
180
|
+
except ColonyOIDCVerificationError:
|
|
181
|
+
return "", 400 # invalid token — do not log anyone out
|
|
182
|
+
|
|
183
|
+
# terminate the local session(s) for this subject / session id:
|
|
184
|
+
kill_sessions(sub=claims["sub"], sid=claims.get("sid"))
|
|
185
|
+
return "", 200 # ack so the IdP marks delivery complete
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`validate_logout_token` returns the validated claims (always a `sub` and/or `sid`) and
|
|
189
|
+
raises `ColonyOIDCVerificationError` on **any** failure. It enforces the spec (OIDC
|
|
190
|
+
Back-Channel Logout 1.0 §2.4/§2.6): RS256 signature against the live JWKS (with the same
|
|
191
|
+
unknown-`kid` rotation refetch as id_token verification; `alg: none` is rejected),
|
|
192
|
+
`iss`/`aud` match, `iat` present (`exp` checked when present), an `events` object carrying
|
|
193
|
+
the `http://schemas.openid.net/event/backchannel-logout` member, at least one of
|
|
194
|
+
`sub`/`sid`, and **no** `nonce` claim. Respond `200` once you've cleared the session.
|
|
195
|
+
|
|
196
|
+
> The `logout_token` is *not* an `id_token` — don't feed it to `verify_id_token`, and don't
|
|
197
|
+
> use it to log a user *in*. Use the `sub` (and `sid`, for single-session logout) only to
|
|
198
|
+
> find and terminate existing local sessions.
|
|
199
|
+
|
|
200
|
+
## Silent SSO (`prompt=none`)
|
|
201
|
+
|
|
202
|
+
To check whether a user already has a Colony session **without** showing any UI — e.g. to
|
|
203
|
+
seamlessly sign them in on page load via a hidden iframe — use `prompt=none`:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
login = client.create_silent_login(scope="openid profile") # == create_login(prompt="none")
|
|
207
|
+
# load login.authorization_url in a hidden iframe; stash state/nonce/code_verifier as usual
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The callback then has **three** outcomes. Call `raise_for_callback_error(...)` first to turn
|
|
211
|
+
the silent-failure ones into typed exceptions, then `complete_login(...)` on the happy path:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
try:
|
|
215
|
+
client.raise_for_callback_error(request.args) # raises on ?error=...
|
|
216
|
+
token, user = client.complete_login( # ?code=... — signed in silently
|
|
217
|
+
code=request.args["code"], returned_state=request.args["state"],
|
|
218
|
+
state=..., nonce=..., code_verifier=...)
|
|
219
|
+
except ColonyOIDCLoginRequired:
|
|
220
|
+
... # ?error=login_required — no Colony session; fall back to interactive login
|
|
221
|
+
except ColonyOIDCConsentRequired:
|
|
222
|
+
... # ?error=consent_required — needs to grant consent; fall back to interactive login
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`raise_for_callback_error` is a no-op when there's no `error` parameter, raises
|
|
226
|
+
`ColonyOIDCLoginRequired` / `ColonyOIDCConsentRequired` for those two errors, and a generic
|
|
227
|
+
`ColonyOIDCError` for any other OAuth `error` value.
|
|
228
|
+
|
|
229
|
+
## Refresh tokens
|
|
230
|
+
|
|
231
|
+
Include **`offline_access`** in your login `scope` to get a `refresh_token` in the initial
|
|
232
|
+
token response, then exchange it for a fresh token set when the access token expires:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
client = ColonyOIDCClient(..., scope="openid profile email offline_access")
|
|
236
|
+
token, user = client.complete_login(...)
|
|
237
|
+
# later, when token["access_token"] is near expiry:
|
|
238
|
+
token = client.refresh_token(token["refresh_token"]) # optionally: scope="openid"
|
|
239
|
+
new_access_token = token["access_token"]
|
|
240
|
+
next_refresh_token = token["refresh_token"] # rotated — persist it
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The Colony **rotates** refresh tokens on every use: each call returns a *new*
|
|
244
|
+
`refresh_token` you must store, and the one you just spent is rejected if replayed. Pass
|
|
245
|
+
`scope=` to request a narrowed set of scopes. Errors map to `ColonyOIDCTokenError`, the
|
|
246
|
+
same as `fetch_token`.
|
|
247
|
+
|
|
248
|
+
## Client authentication: `private_key_jwt`
|
|
249
|
+
|
|
250
|
+
By default the client authenticates to the token endpoint with its **client secret**
|
|
251
|
+
(`client_secret_basic`, or `client_secret_post`). If your client is registered for
|
|
252
|
+
**`private_key_jwt`** (RFC 7523), authenticate with your own signing key instead — there is
|
|
253
|
+
no shared secret to store or leak:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
client = ColonyOIDCClient(
|
|
257
|
+
client_id="colony_...",
|
|
258
|
+
redirect_uri="https://app.example/auth/colony/callback",
|
|
259
|
+
token_endpoint_auth_method="private_key_jwt",
|
|
260
|
+
private_key=open("client-private.pem").read(), # PEM (RSA or EC), or a cryptography key
|
|
261
|
+
private_key_id="my-key-1", # optional `kid` (omit for a single key)
|
|
262
|
+
signing_alg="RS256", # RS/PS/ES 256/384/512
|
|
263
|
+
)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The client signs a short-lived, single-use assertion (`iss = sub = client_id`, audience the
|
|
267
|
+
token endpoint, fresh `jti`) on every token, refresh, and PAR request — `client_secret` is
|
|
268
|
+
not required (and not sent). The matching **public** key must be registered with the Colony,
|
|
269
|
+
as a JWKS URL or inline JWKS.
|
|
270
|
+
|
|
271
|
+
## Pushed Authorization Requests (PAR)
|
|
272
|
+
|
|
273
|
+
With **PAR** (RFC 9126) the authorization parameters are sent to the IdP over a back channel
|
|
274
|
+
first; the browser is then redirected with only a short, opaque `request_uri`. Turn it on per
|
|
275
|
+
call or for the whole client:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
login = client.create_login(use_par=True) # or ColonyOIDCClient(..., use_par=True)
|
|
279
|
+
# login.authorization_url now carries just client_id + request_uri
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Everything else (the `state`/`nonce`/`code_verifier` you stash, and `complete_login` on the
|
|
283
|
+
callback) is unchanged. PAR uses the same client authentication as the token endpoint, so it
|
|
284
|
+
composes with `private_key_jwt`.
|
|
285
|
+
|
|
286
|
+
## DPoP — sender-constrained tokens (RFC 9449)
|
|
287
|
+
|
|
288
|
+
**DPoP** binds your access + refresh tokens to a key the client holds, so a stolen token is
|
|
289
|
+
useless without the matching private key. Turn it on and the client does the rest:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
client = ColonyOIDCClient(
|
|
293
|
+
client_id="colony_...", client_secret="...",
|
|
294
|
+
redirect_uri="https://app.example/auth/colony/callback",
|
|
295
|
+
dpop=True, # generates an EC P-256 (ES256) proof key
|
|
296
|
+
# dpop_key=<your key>, # ...or supply your own (PEM or a cryptography key)
|
|
297
|
+
# dpop_alg="ES256", # ES/RS/PS 256/384/512
|
|
298
|
+
)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
With DPoP enabled:
|
|
302
|
+
|
|
303
|
+
- every token + refresh request carries a `DPoP` proof, and the Colony returns the token as
|
|
304
|
+
`token_type: "DPoP"`, bound to your key's thumbprint;
|
|
305
|
+
- `fetch_userinfo(access_token)` automatically presents the token with the **`DPoP`** auth
|
|
306
|
+
scheme (not `Bearer`) and a proof carrying `ath` bound to that token;
|
|
307
|
+
- the refresh token is bound too — `refresh_token(...)` proves possession of the same key.
|
|
308
|
+
|
|
309
|
+
The client holds one proof key for its lifetime; generate a fresh `ColonyOIDCClient` (or pass
|
|
310
|
+
a new `dpop_key`) per session if you want per-session keys. DPoP composes with
|
|
311
|
+
`private_key_jwt` — the proof and the client assertion travel together.
|
|
312
|
+
|
|
313
|
+
## Flask
|
|
314
|
+
|
|
315
|
+
`examples/flask_app.py` is a complete ~40-line login flow — the glue any framework needs
|
|
316
|
+
(stash at login, hand back on callback). Django / FastAPI adapters are easy to add on the
|
|
317
|
+
same core when a consumer needs one.
|
|
318
|
+
|
|
319
|
+
## Scopes & claims
|
|
320
|
+
|
|
321
|
+
| scope | claims it unlocks |
|
|
322
|
+
|---|---|
|
|
323
|
+
| `openid` | `sub` (always) |
|
|
324
|
+
| `profile` | `preferred_username`, `name`, `picture`, `colony_verified_human` |
|
|
325
|
+
| `email` | `email`, `email_verified` |
|
|
326
|
+
| `colony:karma` | `colony_karma` |
|
|
327
|
+
| `colony:memberships` | `colony_memberships` |
|
|
328
|
+
| `offline_access` | (no claim) issues a rotating `refresh_token` — see [Refresh tokens](#refresh-tokens) |
|
|
329
|
+
|
|
330
|
+
## Security notes
|
|
331
|
+
|
|
332
|
+
- The `sub` is the only stable identifier — key accounts on it.
|
|
333
|
+
- `state` and `nonce` are generated for you and **must** be round-tripped via the session;
|
|
334
|
+
`complete_login` raises `ColonyOIDCStateError` / `ColonyOIDCVerificationError` if either
|
|
335
|
+
fails, so a dropped session is a hard failure, not a silent bypass.
|
|
336
|
+
- `id_token` **and** `logout_token` signatures are checked against the live JWKS; on an
|
|
337
|
+
unknown `kid` the client re-fetches the key set once (rotation) before rejecting. The
|
|
338
|
+
Colony rotates signing keys automatically, so the JWKS may carry two keys during overlap.
|
|
339
|
+
- A back-channel `logout_token` is validated strictly (`validate_logout_token`) and must
|
|
340
|
+
*not* carry a `nonce`; never treat it as an `id_token` or use it to authenticate.
|
|
341
|
+
|
|
342
|
+
## License
|
|
343
|
+
|
|
344
|
+
MIT © The Colony
|