Skip to content

Commit c91489c

Browse files
committed
refactor!: custom flow into configurable oauth
1 parent 3db86f4 commit c91489c

File tree

7 files changed

+954
-176
lines changed

7 files changed

+954
-176
lines changed

README.md

Lines changed: 226 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,235 @@ oidc:
4949
allowed_client_ids: ['2897827328738@project_name']
5050
# Allow registration of new users, defaults to false (optional)
5151
allow_registration: false
52-
custom_flow:
53-
# provide only one of secret, keyfile
54-
secret: symetrical secret
55-
keyfile: path to asymetrical keyfile
56-
57-
# Algorithm of the tokens, defaults to RS256 (optional)
58-
algorithm: RS256
59-
# Require tokens to have an expiry set, defaults to true (optional)
60-
require_expiry: true
61-
# This endpoint will be called when new user is registered
62-
# with `{"token": <token>}` as its request body
63-
notify_on_registration_uri: http://example.com/notify
64-
# Bearer auth token for `notify_on_registration_uri` call (optional)
65-
notification_access_token: 'my$3cr37'
52+
oauth:
53+
# see OAuthConfig section
6654
```
6755
It is recommended to have `require_expiry` set to `true` (default). As for `allow_registration`, it depends on usecase: If you only want to be able to log in *existing* users, leave it at `false` (default). If nonexistant users should be simply registered upon hitting the login endpoint, set it to `true`.
6856

57+
### OAuthConfig:
58+
| Parameter | Type |
59+
|-------------------------------|------------------------------------------------------------------------------|
60+
| `jwt_validation` | [`JwtValidationConfig`](#JwtValidationConfig) (optional) |
61+
| `introspection_validation` | [`IntrospectionValidationConfig`](#IntrospectionValidationConfig) (optional) |
62+
| `username_type` | One of `'fq_uid'`, `'localpart'`, `'user_id'` (optional) |
63+
| `notify_on_registration_url` | String |
64+
| `notify_on_registration_auth` | [`HttpAuth`](#HttpAuth) (optional) |
65+
| `expose_metadata_resource` | Any (optional) |
66+
| `registration_enabled` | Bool (defaults to `false`) |
67+
68+
At least one of `jwt_validation` or `introspection_validation` must be defined.
69+
70+
`username_type` specifies the role of `identifier.user`:
71+
- `'fq_uid'` — must be fully qualified username, e.g. `@alice:example.test`
72+
- `'localpart'` — must be localpart, e.g. `alice`
73+
- `'user_id'` — could be localpart or fully qualified username
74+
- `null` — the username is ignored, it will be source from the token or introspection response
75+
76+
`notify_on_registration_url` will be called when a new user is registered with this body:
77+
```json
78+
{
79+
"localpart": "alice",
80+
"fully_qualified_uid": "@alice:example.test",
81+
"displayname": "Alice",
82+
},
83+
```
84+
85+
`expose_metadata_resource` must be an object with `name` field. The object will be exposed at `/_famedly/login/{expose_metadata_resource.name}`.
86+
87+
`jwt_validation` and `introspection_validation` contain a bunch of `*_path` optional fields. Each of these, if specified will be used to source either localpart, user id, or fully qualified user id from jwt claims and introspection response. They values are going to be compared for equality, if they differ, authentication would fail. Be careful with these, as it is possible to configure in such a way that authentication would always fail, or, if `username_type` is `null`, no user id data can be sourced, thus also leading to failure.
88+
89+
90+
### JwtValidationConfig
91+
[RFC 7519 - JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)
92+
| Parameter | Type |
93+
|--------------------|-----------------------------------------------------------|
94+
| `validator` | [`Validator`](#Validator) (defaults to [`Exist`](#Exist)) |
95+
| `require_expiry` | Bool (defaults to `false`) |
96+
| `localpart_path` | [`Path`](#Path) (optional) |
97+
| `user_id_path` | [`Path`](#Path) (optional) |
98+
| `fq_uid_path` | [`Path`](#Path) (optional) |
99+
| `displayname_path` | [`Path`](#Path) (optional) |
100+
| `required_scopes` | Space separated string or a list of strings (optional) |
101+
| `jwk_set` | [JWKSet](https://datatracker.ietf.org/doc/html/rfc7517#section-5) or [JWK](https://datatracker.ietf.org/doc/html/rfc7517#section-4) (optional) |
102+
| `jwk_file` | String (optional) |
103+
104+
Either `jwk_set` or `jwk_file` must be specified
105+
106+
107+
### IntrospectionValidationConfig
108+
[RFC 7662 - OAuth 2.0 Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
109+
| Parameter | Type |
110+
|--------------------|-----------------------------------------------------------|
111+
| `endpoint` | String |
112+
| `validator` | [`Validator`](#Validator) (defaults to [`Exist`](#Exist)) |
113+
| `auth` | [`HttpAuth`](#HttpAuth) (optional) |
114+
| `localpart_path` | [`Path`](#Path) (optional) |
115+
| `user_id_path` | [`Path`](#Path) (optional) |
116+
| `fq_uid_path` | [`Path`](#Path) (optional) |
117+
| `displayname_path` | [`Path`](#Path) (optional) |
118+
| `required_scopes` | Space separated string or a list of strings (optional) |
119+
120+
### Path
121+
A path is either a string or a list of strings. A path is used to get a value inside a nested dictionary/object.
122+
123+
### BasicAuth
124+
| Parameter | Type |
125+
|------------|--------|
126+
| `username` | String |
127+
| `password` | String |
128+
129+
### BearerAuth
130+
| Parameter | Type |
131+
|-----------|--------|
132+
| `token` | String |
133+
134+
### HttpAuth
135+
Authentication options, always optional
136+
| Parameter | Type |
137+
|-----------|-------------------------|
138+
| `type` | `'basic'` \| `'bearer'` |
139+
140+
Possible options: [`BasicAuth`](#BasicAuth), [`BearerAuth`](#BearerAuth),
141+
142+
### Validator
143+
A validator is any of these types:
144+
[`Exist`](#Exist),
145+
[`Not`](#Not),
146+
[`Equal`](#Equal),
147+
[`MatchesRegex`](#MatchesRegex),
148+
[`AnyOf`](#AnyOf),
149+
[`AllOf`](#AllOf),
150+
[`In`](#In),
151+
[`ListAnyOf`](#ListAnyOf),
152+
[`ListAllOf`](#ListAllOf)
153+
154+
Each validator has `type` field
155+
156+
### Exist
157+
Validator that always returns true
158+
159+
#### Examples
160+
```yaml
161+
{'type': 'exist'}
162+
```
163+
or
164+
```yaml
165+
['exist']
166+
```
167+
168+
### Not
169+
Validator that inverses the result of the inner validator
170+
171+
| Parameter | Type |
172+
|-------------|---------------------------|
173+
| `validator` | [`Validator`](#Validator) |
174+
175+
#### Examples
176+
```yaml
177+
{'type': 'not', 'validator': 'exist'}
178+
```
179+
or
180+
```yaml
181+
['not', 'exist']
182+
```
183+
184+
### Equal
185+
Validator that checks for equality with a constant
186+
187+
| Parameter | Type |
188+
|-----------|-------|
189+
| `value` | `Any` |
190+
191+
#### Examples
192+
```yaml
193+
{'type': 'equal', 'value': 3}
194+
```
195+
or
196+
```yaml
197+
['equal', 3]
198+
```
199+
200+
### MatchesRegex
201+
Validator that checks if a value is string and matches given regex
202+
203+
| Parameter | Type | Description |
204+
|--------------------------------------------|--------|-----------------------------|
205+
| `regex` | `str` | Python regex syntax |
206+
| `full_match` (optional, `true` by default) | `bool` | Full match or partial match |
207+
208+
#### Examples
209+
```yaml
210+
{'type': 'regex', 'value': 'hello.'}
211+
```
212+
or
213+
```yaml
214+
['regex', 'hello.', false]
215+
```
216+
217+
### AnyOf
218+
Validator that checks if **any** of the inner validators passes
219+
220+
221+
| Parameter | Type |
222+
|--------------|-----------------------------------|
223+
| `validators` | List of [`Validator`](#Validator) |
224+
225+
#### Examples
226+
```yaml
227+
type: any_of
228+
validators:
229+
- ['in', 'foo', ['equal', 3]]
230+
- ['in', 'bar' ['exist']]
231+
```
232+
or
233+
```yaml
234+
['any_of', [['in', 'bar' ['exist']], ['in', 'foo', ['equal', 3]]]]
235+
```
236+
237+
### AllOf
238+
Validator that checks if **all** of the inner validators pass
239+
240+
| Parameter | Type |
241+
|--------------|-----------------------------------|
242+
| `validators` | List of [`Validator`](#Validator) |
243+
244+
#### Examples
245+
```yaml
246+
type: all_of
247+
validators:
248+
- ['exist']
249+
- ['in', 'foo', ['equal', 3]]
250+
```
251+
or
252+
```yaml
253+
['all_of', [['exist'], ['in', 'foo', ['equal', 3]]]]
254+
```
255+
256+
### In
257+
Validator that modifies the context for the inner validator, *going inside* a dict key.
258+
If the validated object is not a dict, or doesn't have specivied `path`, validation failes. Validator
259+
260+
| Parameter | Type |
261+
|-------------|---------------------------------------------------------------------|
262+
| `path` | [`Path`](#Path) |
263+
| `validator` | [`Validator`](#Validator) (optional, defaults to [`Exist`](#Exist)) |
264+
265+
#### Examples
266+
```yaml
267+
['in', ['foo', 'bar'], ['equal', 3]]
268+
```
269+
270+
### ListAllOf:
271+
*TODO*
272+
273+
### ListAnyOf:
274+
*TODO*
275+
276+
#### Examples
277+
- `'foo'` is an existing path in `{'foo': 3}`, resulting in value `3`
278+
- `['foo']` is an existing path in `{'foo': 3}`, resulting in value `3`
279+
- `['foo', 'bar']` is an existing path in `{'foo': {'bar': 3}}`, resulting in value `3`
280+
69281
## Usage
70282

71283
### JWT Authentication
@@ -110,38 +322,6 @@ Next, the client needs to use these tokens and construct a payload to the login
110322
}
111323
```
112324

113-
### Custom flow
114-
115-
This is similar to jwt flow except few additinal claims are checked:
116-
- `name` claim must be present
117-
- `urn:messaging:matrix:localpart` claim must be equal to user name
118-
- `urn:messaging:matrix:mxid` claim must be valid mxid with localpart matching `urn:messaging:matrix:localpart` claim and domain name matching this homeserver domain
119-
120-
```jsonc
121-
{
122-
"type": "com.famedly.login.token.custom",
123-
"identifier": {
124-
"type": "m.id.user",
125-
"user": "d2773fdb-91b5-4e77-9367-d4bd121afc48" // localpart, same as `urn:messaging:matrix:localpart` in JWT
126-
},
127-
"token": "<jwt IDToken here>"
128-
}
129-
```
130-
131-
An example of a JWT payload:
132-
```jsonc
133-
{
134-
"iss": "https://auth.example.com",
135-
"sub": "8fd1ec9b-c054-4de0-bbd0-90d40ce9200e",
136-
"exp": 1701432906,
137-
"urn:messaging:matrix:mxid": "@d2773fdb-91b5-4e77-9367-d4bd121afc48:homserver.matrix.de",
138-
"urn:messaging:matrix:localpart": "d2773fdb-91b5-4e77-9367-d4bd121afc48",
139-
"name": "Alice Bob"
140-
}
141-
```
142-
143-
Additionally, when a new user is registered, a POST json request is made with `{"token": <token>}` as its request body. The handler of the request must return any json due to some implementation details (synapse's `BaseHttpClient` poor interface)
144-
145325
## Testing
146326

147327
The tests uses twisted's testing framework trial, with the development

0 commit comments

Comments
 (0)