@toa.io/extensions.exposition 0.20.0-dev.9 → 0.20.0
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.
- package/components/context.toa.yaml +15 -0
- package/components/identity.bans/manifest.toa.yaml +18 -0
- package/components/identity.basic/events/principal.js +9 -0
- package/components/identity.basic/manifest.toa.yaml +50 -0
- package/components/identity.basic/source/authenticate.ts +29 -0
- package/components/identity.basic/source/create.ts +19 -0
- package/components/identity.basic/source/transit.ts +64 -0
- package/components/identity.basic/source/types.ts +42 -0
- package/components/identity.basic/tsconfig.json +9 -0
- package/components/identity.roles/manifest.toa.yaml +31 -0
- package/components/identity.roles/source/list.ts +7 -0
- package/components/identity.roles/source/principal.ts +20 -0
- package/components/identity.roles/tsconfig.json +9 -0
- package/components/identity.tokens/manifest.toa.yaml +39 -0
- package/components/identity.tokens/receivers/identity.bans.updated.js +3 -0
- package/components/identity.tokens/source/authenticate.test.ts +56 -0
- package/components/identity.tokens/source/authenticate.ts +38 -0
- package/components/identity.tokens/source/decrypt.test.ts +59 -0
- package/components/identity.tokens/source/decrypt.ts +25 -0
- package/components/identity.tokens/source/encrypt.test.ts +35 -0
- package/components/identity.tokens/source/encrypt.ts +25 -0
- package/components/identity.tokens/source/revoke.ts +5 -0
- package/components/identity.tokens/source/types.ts +48 -0
- package/components/identity.tokens/tsconfig.json +9 -0
- package/cucumber.js +9 -0
- package/documentation/.assets/ia3-dark.jpg +0 -0
- package/documentation/.assets/ia3-light.jpg +0 -0
- package/documentation/.assets/overview-dark.jpg +0 -0
- package/documentation/.assets/overview-light.jpg +0 -0
- package/documentation/.assets/role-scopes-dark.jpg +0 -0
- package/documentation/.assets/role-scopes-light.jpg +0 -0
- package/documentation/.assets/rtd-dark.jpg +0 -0
- package/documentation/.assets/rtd-light.jpg +0 -0
- package/documentation/access.md +256 -0
- package/documentation/components.md +276 -0
- package/documentation/identity.md +156 -0
- package/documentation/notes/sse.md +71 -0
- package/documentation/protocol.md +18 -0
- package/documentation/query.md +226 -0
- package/documentation/tree.md +169 -0
- package/features/access.feature +448 -0
- package/features/annotation.feature +30 -0
- package/features/body.feature +45 -0
- package/features/directives.feature +56 -0
- package/features/dynamic.feature +99 -0
- package/features/errors.feature +193 -0
- package/features/identity.basic.feature +276 -0
- package/features/identity.feature +61 -0
- package/features/identity.roles.feature +51 -0
- package/features/identity.tokens.feature +119 -0
- package/features/queries.feature +214 -0
- package/features/routes.feature +49 -0
- package/features/steps/Common.ts +10 -0
- package/features/steps/Components.ts +43 -0
- package/features/steps/Database.ts +58 -0
- package/features/steps/Gateway.ts +113 -0
- package/features/steps/HTTP.ts +71 -0
- package/features/steps/Parameters.ts +12 -0
- package/features/steps/Workspace.ts +40 -0
- package/features/steps/components/echo/manifest.toa.yaml +9 -0
- package/features/steps/components/echo/operations/affect.js +7 -0
- package/features/steps/components/echo/operations/compute.js +7 -0
- package/features/steps/components/greeter/manifest.toa.yaml +5 -0
- package/features/steps/components/greeter/operations/greet.js +7 -0
- package/features/steps/components/pots/manifest.toa.yaml +20 -0
- package/features/steps/components/sequences/manifest.toa.yaml +10 -0
- package/features/steps/components/sequences/operations/numbers.js +7 -0
- package/features/steps/components/sequences/operations/tokens.js +16 -0
- package/features/steps/components/users/manifest.toa.yaml +11 -0
- package/features/steps/tsconfig.json +9 -0
- package/features/streams.feature +26 -0
- package/package.json +32 -17
- package/readme.md +183 -0
- package/schemas/annotation.cos.yaml +5 -0
- package/schemas/directive.cos.yaml +3 -0
- package/schemas/method.cos.yaml +8 -0
- package/schemas/node.cos.yaml +5 -0
- package/schemas/query.cos.yaml +17 -0
- package/schemas/querystring.cos.yaml +5 -0
- package/schemas/range.cos.yaml +2 -0
- package/schemas/route.cos.yaml +2 -0
- package/source/Annotation.ts +7 -0
- package/source/Branch.ts +8 -0
- package/source/Composition.ts +57 -0
- package/source/Context.ts +6 -0
- package/source/Directive.test.ts +91 -0
- package/source/Directive.ts +120 -0
- package/source/Endpoint.ts +59 -0
- package/source/Factory.ts +51 -0
- package/source/Gateway.ts +93 -0
- package/source/HTTP/Server.fixtures.ts +45 -0
- package/source/HTTP/Server.test.ts +221 -0
- package/source/HTTP/Server.ts +135 -0
- package/source/HTTP/exceptions.ts +77 -0
- package/source/HTTP/formats/index.ts +19 -0
- package/source/HTTP/formats/json.ts +13 -0
- package/source/HTTP/formats/msgpack.ts +10 -0
- package/source/HTTP/formats/text.ts +9 -0
- package/source/HTTP/formats/yaml.ts +14 -0
- package/source/HTTP/index.ts +3 -0
- package/source/HTTP/messages.test.ts +116 -0
- package/source/HTTP/messages.ts +89 -0
- package/source/Mapping.ts +51 -0
- package/source/Query.test.ts +37 -0
- package/source/Query.ts +105 -0
- package/source/RTD/Context.ts +16 -0
- package/source/RTD/Directives.ts +9 -0
- package/source/RTD/Endpoint.ts +11 -0
- package/source/RTD/Match.ts +16 -0
- package/source/RTD/Method.ts +24 -0
- package/source/RTD/Node.ts +85 -0
- package/source/RTD/Route.ts +59 -0
- package/source/RTD/Tree.ts +54 -0
- package/source/RTD/factory.ts +47 -0
- package/source/RTD/index.ts +8 -0
- package/source/RTD/segment.test.ts +32 -0
- package/source/RTD/segment.ts +25 -0
- package/source/RTD/syntax/index.ts +2 -0
- package/source/RTD/syntax/parse.test.ts +188 -0
- package/source/RTD/syntax/parse.ts +153 -0
- package/source/RTD/syntax/types.ts +48 -0
- package/source/Remotes.test.ts +42 -0
- package/source/Remotes.ts +22 -0
- package/source/Tenant.ts +38 -0
- package/source/deployment.ts +49 -0
- package/source/directives/auth/Anonymous.ts +14 -0
- package/source/directives/auth/Echo.ts +12 -0
- package/source/directives/auth/Family.ts +145 -0
- package/source/directives/auth/Id.ts +19 -0
- package/source/directives/auth/Incept.ts +42 -0
- package/source/directives/auth/Role.test.ts +62 -0
- package/source/directives/auth/Role.ts +56 -0
- package/source/directives/auth/Rule.ts +28 -0
- package/source/directives/auth/Scheme.ts +26 -0
- package/source/directives/auth/index.ts +3 -0
- package/source/directives/auth/schemes.ts +8 -0
- package/source/directives/auth/split.ts +15 -0
- package/source/directives/auth/types.ts +37 -0
- package/source/directives/dev/Family.ts +36 -0
- package/source/directives/dev/Stub.ts +14 -0
- package/source/directives/dev/Throw.ts +14 -0
- package/source/directives/dev/index.ts +3 -0
- package/source/directives/dev/types.ts +5 -0
- package/source/directives/index.ts +5 -0
- package/source/discovery.ts +1 -0
- package/source/exceptions.ts +17 -0
- package/source/index.test.ts +9 -0
- package/source/index.ts +6 -0
- package/source/manifest.test.ts +59 -0
- package/source/manifest.ts +48 -0
- package/source/root.ts +38 -0
- package/source/schemas.ts +9 -0
- package/tsconfig.json +12 -0
- package/src/.manifest/index.js +0 -7
- package/src/.manifest/normalize.js +0 -58
- package/src/.manifest/schema.yaml +0 -71
- package/src/.manifest/validate.js +0 -17
- package/src/constants.js +0 -3
- package/src/deployment.js +0 -23
- package/src/exposition.js +0 -68
- package/src/factory.js +0 -76
- package/src/index.js +0 -9
- package/src/manifest.js +0 -12
- package/src/query/criteria.js +0 -55
- package/src/query/enum.js +0 -35
- package/src/query/index.js +0 -17
- package/src/query/query.js +0 -60
- package/src/query/range.js +0 -28
- package/src/query/sort.js +0 -19
- package/src/remote.js +0 -88
- package/src/server.js +0 -83
- package/src/tenant.js +0 -29
- package/src/translate/etag.js +0 -14
- package/src/translate/index.js +0 -7
- package/src/translate/request.js +0 -68
- package/src/translate/response.js +0 -62
- package/src/tree.js +0 -109
- package/test/manifest.normalize.fixtures.js +0 -37
- package/test/manifest.normalize.test.js +0 -37
- package/test/manifest.validate.test.js +0 -40
- package/test/query.range.test.js +0 -18
- package/test/tree.fixtures.js +0 -21
- package/test/tree.test.js +0 -44
- package/types/annotations.d.ts +0 -10
- package/types/declarations.d.ts +0 -31
- package/types/exposition.d.ts +0 -13
- package/types/http.d.ts +0 -13
- package/types/query.d.ts +0 -16
- package/types/remote.d.ts +0 -19
- package/types/server.d.ts +0 -13
- package/types/tree.d.ts +0 -33
package/cucumber.js
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Access authorization
|
|
2
|
+
|
|
3
|
+
> The Authorization is intrinsically linked with the [Authentication](./identity.md).
|
|
4
|
+
|
|
5
|
+
<a href="">
|
|
6
|
+
<picture>
|
|
7
|
+
<source media="(prefers-color-scheme: dark)" srcset="./.assets/ia3-dark.jpg">
|
|
8
|
+
<img alt="IA3" width="600" height="507" src="./.assets/ia3-light.jpg">
|
|
9
|
+
</picture>
|
|
10
|
+
</a>
|
|
11
|
+
|
|
12
|
+
## Directives
|
|
13
|
+
|
|
14
|
+
The Authorization is implemented as a set of [RTD Directives](tree.md#directives).
|
|
15
|
+
|
|
16
|
+
Directives are executed in a predetermined order until one of them grants access to a resource. If
|
|
17
|
+
none of the
|
|
18
|
+
directives grants access, then the Authorization interrupts request processing and responds with an
|
|
19
|
+
authorization error.
|
|
20
|
+
|
|
21
|
+
> The Authorization directive provider is named `authorization`,
|
|
22
|
+
> so the full names of the directives are `authorization:{directive}`.
|
|
23
|
+
|
|
24
|
+
### `anonymous`
|
|
25
|
+
|
|
26
|
+
Grants access if its value is `true` and no credentials were provided[^1].
|
|
27
|
+
|
|
28
|
+
[^1]: Credentials in the request make the
|
|
29
|
+
response [non-chachable](https://datatracker.ietf.org/doc/html/rfc7234#section-3).
|
|
30
|
+
|
|
31
|
+
### `id`
|
|
32
|
+
|
|
33
|
+
Grants access if resolved Identity matches the value of the URL path segment placeholder named after
|
|
34
|
+
the directive's value.
|
|
35
|
+
|
|
36
|
+
#### Example
|
|
37
|
+
|
|
38
|
+
Given the Route declaration and corresponding HTTP request:
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
# context.toa.yaml
|
|
42
|
+
|
|
43
|
+
exposition:
|
|
44
|
+
/users/:user-id:
|
|
45
|
+
id: "user-id"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```http
|
|
49
|
+
GET /users/87480f2bd88048518c529d7957475ecd/
|
|
50
|
+
Authorization: ...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For this request access will be granted if the resolved Identity value
|
|
54
|
+
is `87480f2bd88048518c529d7957475ecd`.
|
|
55
|
+
|
|
56
|
+
### `role`
|
|
57
|
+
|
|
58
|
+
Grants access if resolved Identity has a role matching the directive's value or one of its values.
|
|
59
|
+
|
|
60
|
+
#### Example
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
# context.toa.yaml
|
|
64
|
+
|
|
65
|
+
exposition:
|
|
66
|
+
/code:
|
|
67
|
+
role: [developer, reviewer]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Access will be granted if the resolved Identity has a role that matches `developer` or `reviewer`.
|
|
71
|
+
|
|
72
|
+
Read [Roles](#roles) section for more details.
|
|
73
|
+
|
|
74
|
+
### `rule`
|
|
75
|
+
|
|
76
|
+
The Rule is a collection of authorization directives. It allows access only if all the specified
|
|
77
|
+
directives grant
|
|
78
|
+
access. The value of the `rule` directive can be a single Rule or a list of Rules.
|
|
79
|
+
|
|
80
|
+
#### Example
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
# context.toa.yaml
|
|
84
|
+
|
|
85
|
+
exposition:
|
|
86
|
+
/commits/:user-id:
|
|
87
|
+
rule:
|
|
88
|
+
id: user-id
|
|
89
|
+
role: developer
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Access will be granted if an Identity matches a `user-id` placeholder and has a Role of `developer`.
|
|
93
|
+
|
|
94
|
+
## Roles
|
|
95
|
+
|
|
96
|
+
Role values are strings that can be assigned to an Identity and used for matching with values of
|
|
97
|
+
the [`role` directive](#role).
|
|
98
|
+
|
|
99
|
+
### Hierarchy
|
|
100
|
+
|
|
101
|
+
Role values are alphanumeric tokens separated by a colon (`:`).
|
|
102
|
+
Each token defines a Role Scope, forming a hierarchy.
|
|
103
|
+
A Role matches the value of the `rule` directive if that Role has the specified Scope in a
|
|
104
|
+
directive.
|
|
105
|
+
|
|
106
|
+
#### Example
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
# context.toa.yaml
|
|
110
|
+
|
|
111
|
+
/exposition:
|
|
112
|
+
/commits/:user-id:
|
|
113
|
+
role: developer:senior
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The example above defines a `role` directive with the specified `developer:senior` Role Scope.
|
|
117
|
+
This directive matches the roles `developer:senior` and `developer`,
|
|
118
|
+
but it **does not** match the Role `developer:senior:javascript`.
|
|
119
|
+
In other words, the Identity must have a specified or more general Role.
|
|
120
|
+
|
|
121
|
+
<a href="https://miro.com/app/board/uXjVOoy0ImU=/?moveToWidget=3458764556008550471&cot=14">
|
|
122
|
+
<picture>
|
|
123
|
+
<source media="(prefers-color-scheme: dark)" srcset=".assets/role-scopes-dark.jpg">
|
|
124
|
+
<img alt="IA3" width="600" height="425" src=".assets/role-scopes-light.jpg">
|
|
125
|
+
</picture>
|
|
126
|
+
</a>
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
> The root-level Role Scope `system` is preserved and cannot be used with the `role` directives.
|
|
130
|
+
|
|
131
|
+
See also [role management resources](components.md#roles).
|
|
132
|
+
|
|
133
|
+
#### Authorization Directives
|
|
134
|
+
|
|
135
|
+
```yaml
|
|
136
|
+
/identity/roles/:id:
|
|
137
|
+
role: system:roles
|
|
138
|
+
````
|
|
139
|
+
|
|
140
|
+
## Policies
|
|
141
|
+
|
|
142
|
+
Component Resource branches cannot have authorization directives.
|
|
143
|
+
Instead, they must declare Authorization Policies using `policy` directive to
|
|
144
|
+
be attached in the Context to a Resource Tree as a set of Authorization Directives
|
|
145
|
+
using `attachment` directive.
|
|
146
|
+
|
|
147
|
+
This restriction provides a separation of concerns, allowing components to be reused in different
|
|
148
|
+
Contexts with varying
|
|
149
|
+
access rules.
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
# manifest.toa.yaml
|
|
153
|
+
|
|
154
|
+
name: posts
|
|
155
|
+
|
|
156
|
+
exposition:
|
|
157
|
+
/:user-id:
|
|
158
|
+
GET:
|
|
159
|
+
endpoint: observe
|
|
160
|
+
policy: read:list
|
|
161
|
+
POST:
|
|
162
|
+
endpoint: transit
|
|
163
|
+
policy: post:submit
|
|
164
|
+
/:post-id:
|
|
165
|
+
GET:
|
|
166
|
+
endpoint: observe
|
|
167
|
+
policy: read:post
|
|
168
|
+
PUT:
|
|
169
|
+
endpoint: assign
|
|
170
|
+
policy: post:edit
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```yaml
|
|
174
|
+
# context.toa.yaml
|
|
175
|
+
|
|
176
|
+
exposition:
|
|
177
|
+
/posts:
|
|
178
|
+
attachment:
|
|
179
|
+
read:
|
|
180
|
+
anonymous: true
|
|
181
|
+
post:
|
|
182
|
+
id: user-id
|
|
183
|
+
post:edit:
|
|
184
|
+
role: app:posts:editor
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Policy values as well as [Role](#roles) values define hierarchical Policy Scopes.
|
|
188
|
+
|
|
189
|
+
In the example above:
|
|
190
|
+
|
|
191
|
+
- an Attachment `read` attaches Directive `anonymous: true` to both `read:list` and `read:post`
|
|
192
|
+
Policy Scopes.
|
|
193
|
+
This means that a list of posts and each post can be accessed without authorization.
|
|
194
|
+
- an Attachment `post` attaches Directive `id: user-id` to both `post:submit` and `post:edit` Policy
|
|
195
|
+
Scopes.
|
|
196
|
+
This means that an Identity can submit and edit their own posts.
|
|
197
|
+
- an Attachment `post:edit` attaches Directive `role: app:posts:editor` to `post:edit` Policy Scope.
|
|
198
|
+
This means that an identity with the role scope `app:posts:editor` can edit posts by any author,
|
|
199
|
+
in addition to the fact that the author themselves can do this thanks to the previous Attachment.
|
|
200
|
+
|
|
201
|
+
### Nesting
|
|
202
|
+
|
|
203
|
+
Policies are namespace-scoped, meaning they can be attached to any Route under the
|
|
204
|
+
corresponding `/{namespace}` prefix.
|
|
205
|
+
|
|
206
|
+
Attachment is applied to the node where it is declared, as well as its nested nodes.
|
|
207
|
+
Directives of the Attachment are applied to the node where the attached Policies are declared, as
|
|
208
|
+
well as their nested nodes.
|
|
209
|
+
|
|
210
|
+
Here's an example of how this works:
|
|
211
|
+
|
|
212
|
+
```yaml
|
|
213
|
+
# manifest.toa.yaml
|
|
214
|
+
|
|
215
|
+
name: posts
|
|
216
|
+
|
|
217
|
+
exposition:
|
|
218
|
+
/:user-id:
|
|
219
|
+
GET:
|
|
220
|
+
endpoint: observe
|
|
221
|
+
policy: read
|
|
222
|
+
/:user-id/:post-id:
|
|
223
|
+
GET:
|
|
224
|
+
endpoint: observe
|
|
225
|
+
policy: read
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```yaml
|
|
229
|
+
# context.toa.yaml
|
|
230
|
+
|
|
231
|
+
exposition:
|
|
232
|
+
/posts:
|
|
233
|
+
/:user-id:
|
|
234
|
+
attachment:
|
|
235
|
+
read:
|
|
236
|
+
anonymous: true
|
|
237
|
+
/:user-id/:post-id:
|
|
238
|
+
attachment:
|
|
239
|
+
read:
|
|
240
|
+
role: reader
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
In the example above, the same Policy `read` is attached to two Routes with different Directives.
|
|
244
|
+
|
|
245
|
+
The following example demonstrates the attachment of the `read` Policy to both Routes with the same
|
|
246
|
+
Directive:
|
|
247
|
+
|
|
248
|
+
```yaml
|
|
249
|
+
# context.toa.yaml
|
|
250
|
+
|
|
251
|
+
exposition:
|
|
252
|
+
/posts:
|
|
253
|
+
attachment:
|
|
254
|
+
read:
|
|
255
|
+
anonymous: true
|
|
256
|
+
```
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Components and resources
|
|
2
|
+
|
|
3
|
+
Exposition comes with a set of components that run within the same process. These components are
|
|
4
|
+
configured in the same
|
|
5
|
+
way as if they were a part of the Context. Resources exposed by the components
|
|
6
|
+
are [isolated](tree.md#directives).
|
|
7
|
+
|
|
8
|
+
## Basic credentials
|
|
9
|
+
|
|
10
|
+
The `identity.basic` component stores basic credentials.
|
|
11
|
+
|
|
12
|
+
### Password hashing
|
|
13
|
+
|
|
14
|
+
Passwords are hashed using the [bcrypt](https://github.com/dcodeIO/bcrypt.js) algorithm with salt
|
|
15
|
+
and pepper.
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# context.toa.yaml
|
|
19
|
+
|
|
20
|
+
configuration:
|
|
21
|
+
identity.basic:
|
|
22
|
+
rounds: 10 # salt rounds
|
|
23
|
+
peper: '' # hashing pepper
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Credentials constraints
|
|
27
|
+
|
|
28
|
+
Credential constraints are defined using a set of regular expressions (values must match all of
|
|
29
|
+
them).
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
# context.toa.yaml
|
|
33
|
+
|
|
34
|
+
configuration:
|
|
35
|
+
identity.basic:
|
|
36
|
+
username:
|
|
37
|
+
- ^\S{1,16}$
|
|
38
|
+
password:
|
|
39
|
+
- ^\S{8,32}$
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> Values in the example above are the default values.
|
|
43
|
+
|
|
44
|
+
### Principal
|
|
45
|
+
|
|
46
|
+
When an application is deployed for the first time, there are no credentials, and therefore, there
|
|
47
|
+
is no Identity that
|
|
48
|
+
could have a Role to manage Roles of other Identities.
|
|
49
|
+
|
|
50
|
+
This issue is addressed by using the `principal` key in the configuration:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
# context.toa.yaml
|
|
54
|
+
|
|
55
|
+
configuration:
|
|
56
|
+
identity.basic:
|
|
57
|
+
principal: root
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The value of the `principal` key corresponds to the `username` of the basic credentials. Once these
|
|
61
|
+
credentials are
|
|
62
|
+
created, the associated Identity will be assigned the `system` Role.
|
|
63
|
+
|
|
64
|
+
Once created, the username of the principal cannot be modified.
|
|
65
|
+
|
|
66
|
+
### Resources
|
|
67
|
+
|
|
68
|
+
#### `/identity/basic/`
|
|
69
|
+
|
|
70
|
+
<code>POST</code> Create new Identity with Basic credentials. Request body is as follows:
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
username: string
|
|
74
|
+
password: string
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Access is [anonymous](access.md#anonymous).
|
|
78
|
+
|
|
79
|
+
#### `/identity/basic/:id/`
|
|
80
|
+
|
|
81
|
+
> `:id` placeholder refers to an Identity.
|
|
82
|
+
|
|
83
|
+
<code>PUT</code> Update basic credentials. Request body is as follows:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
username?: string
|
|
87
|
+
password?: string
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Access requires basic credentials of the modified Identity or `system:identity:basic` role.
|
|
91
|
+
|
|
92
|
+
## Stateless tokens
|
|
93
|
+
|
|
94
|
+
The `identity.tokens` component manages statless authentication tokens.
|
|
95
|
+
|
|
96
|
+
These tokens carry the information required to authenticate the Identity and authorize access.
|
|
97
|
+
|
|
98
|
+
### Issuing tokens
|
|
99
|
+
|
|
100
|
+
The new token is issued each time the request is made:
|
|
101
|
+
|
|
102
|
+
1. Using authentication scheme other than `Token`.
|
|
103
|
+
2. Using `Token` authentication scheme with an [obsolete token](#token-rotation).
|
|
104
|
+
|
|
105
|
+
### Token encryption
|
|
106
|
+
|
|
107
|
+
Issued tokens are encrypted
|
|
108
|
+
with [PASETO V3 encryption](https://github.com/panva/paseto/blob/main/docs/README.md#v3encryptpayload-key-options)
|
|
109
|
+
using the `key0` configuration value as a secret.
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
# context.toa.yaml
|
|
113
|
+
|
|
114
|
+
configuration:
|
|
115
|
+
identity.basic:
|
|
116
|
+
key0: $TOKEN_ENCRYPTION_KEY
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The `key0` configuration value is required.
|
|
120
|
+
|
|
121
|
+
> Valid secret key may be generated using the [`toa key` command](/runtime/cli/readme.md#key).
|
|
122
|
+
|
|
123
|
+
### Token rotation
|
|
124
|
+
|
|
125
|
+
Issued tokens are valid for a `lifetime` period defined in the configuration. After the `refresh`
|
|
126
|
+
period, the token is
|
|
127
|
+
considered obsolete (yet still valid), and a new token is [issued](#issuing-tokens) unless the
|
|
128
|
+
provided one has
|
|
129
|
+
been [revoked](#token-revocation).
|
|
130
|
+
|
|
131
|
+
This essentially means that if the client uses the token at least once every `lifetime` period, it
|
|
132
|
+
will always have a
|
|
133
|
+
valid token to authenticate with. Also, token revocation or changing roles of an Identity will take
|
|
134
|
+
effect once
|
|
135
|
+
the `refresh` period of the currently issued tokens has expired.
|
|
136
|
+
|
|
137
|
+
Adjusting these two values is a delicate trade-off between security, performance and client
|
|
138
|
+
convinience.
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
# context.toa.yaml
|
|
142
|
+
|
|
143
|
+
configuration:
|
|
144
|
+
identity.basic:
|
|
145
|
+
lifetime: 2592000 # seconds, 30 days
|
|
146
|
+
refresh: 600 # seconds, 10 minutes
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
> Values in the example above are the default values.
|
|
150
|
+
|
|
151
|
+
### Token revocation
|
|
152
|
+
|
|
153
|
+
All currently issued tokens of an Identity are revoked when:
|
|
154
|
+
|
|
155
|
+
1. Basic credentials associated with the Identity are [modified](#identitybasicid).
|
|
156
|
+
2. Identity is [banned](#banned-identities).
|
|
157
|
+
|
|
158
|
+
Token revocation takes effect once the `refresh` period of the currently issued tokens has expired.
|
|
159
|
+
|
|
160
|
+
### Secret rotation
|
|
161
|
+
|
|
162
|
+
Tokens are always encrypted using the `key0` configuration value, and they will be decrypted by
|
|
163
|
+
attempting both
|
|
164
|
+
the `key0` and `key1` values in order.
|
|
165
|
+
|
|
166
|
+
`key0` is considered the "current key," and `key1` is considered the "previous key."
|
|
167
|
+
|
|
168
|
+
```yaml
|
|
169
|
+
# context.toa.yaml
|
|
170
|
+
|
|
171
|
+
configuration:
|
|
172
|
+
identity.basic:
|
|
173
|
+
key0: $TOKEN_ENCRYPTION_KEY_2023Q3
|
|
174
|
+
key1: $TOKEN_ENCRYPTION_KEY_2023Q2
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Secret rotation is performed by adding a new key as the `key0` value and moving the existing `key0`
|
|
178
|
+
to the `key1` value.
|
|
179
|
+
|
|
180
|
+
When rolling out the new secret key, there will be a period of time when the new key is deployed to
|
|
181
|
+
some Exposition
|
|
182
|
+
instances. During this time, these instances will start using the new key to encrypt tokens, while
|
|
183
|
+
other instances will
|
|
184
|
+
continue using the current key and will not be able to decrypt tokens encrypted with the new key.
|
|
185
|
+
|
|
186
|
+
To address this issue, the `key1` configuration value may be used as a "transient key."
|
|
187
|
+
|
|
188
|
+
The secret rotation is a 2-step process:
|
|
189
|
+
|
|
190
|
+
> The process **must not** be performed earlier than the `lifetime` period since the last rotation,
|
|
191
|
+
> as it may invalidate
|
|
192
|
+
> tokens before they expire. Therefore, it is guaranteed that there are no valid tokens issued with
|
|
193
|
+
> the current `key1`
|
|
194
|
+
> value.
|
|
195
|
+
|
|
196
|
+
1. Deploy the new secret key to all Exposition instances as `key1`. This enables all instances to
|
|
197
|
+
decrypt tokens
|
|
198
|
+
encrypted with the new key while still using the current key for encryption.
|
|
199
|
+
|
|
200
|
+
```yaml
|
|
201
|
+
# context.toa.yaml
|
|
202
|
+
|
|
203
|
+
configuration:
|
|
204
|
+
identity.basic:
|
|
205
|
+
key0: $TOKEN_ENCRYPTION_KEY_2023Q3
|
|
206
|
+
key1: $TOKEN_ENCRYPTION_KEY_2023Q4
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
2. Move the new secret key from `key1` to `key0`, and move the current key from `key0` to `key1`.
|
|
210
|
+
During this rollout,
|
|
211
|
+
all instances can decrypt tokens encrypted with both the new key and the current key.
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
# context.toa.yaml
|
|
215
|
+
|
|
216
|
+
configuration:
|
|
217
|
+
identity.basic:
|
|
218
|
+
key0: $TOKEN_ENCRYPTION_KEY_2023Q4
|
|
219
|
+
key1: $TOKEN_ENCRYPTION_KEY_2023Q3
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Roles
|
|
223
|
+
|
|
224
|
+
The `identity.roles` component manages roles of an Identity used by [access authorization](access.md#role).
|
|
225
|
+
|
|
226
|
+
### Role resources
|
|
227
|
+
|
|
228
|
+
#### `/identity/roles/:id/`
|
|
229
|
+
|
|
230
|
+
`GET` Get roles of an Identity.
|
|
231
|
+
|
|
232
|
+
Access requires credentials of the Identity or `system:identity:roles` role.
|
|
233
|
+
|
|
234
|
+
`POST` Add a role to an Identity. Request body is as follows:
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
role: string
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Access requires `system:identity:roles` role.
|
|
241
|
+
|
|
242
|
+
## Banned Identities
|
|
243
|
+
|
|
244
|
+
The `identity.bans` component manages banned identities.
|
|
245
|
+
A banned identity will fail to authenticate with any associated credentials (except [tokens](#stateless-tokens) within
|
|
246
|
+
the `refresh` period).
|
|
247
|
+
|
|
248
|
+
```http
|
|
249
|
+
PUT /identity/bans/:id/
|
|
250
|
+
authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
|
|
251
|
+
content-type: application/yaml
|
|
252
|
+
|
|
253
|
+
banned: true
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Access requires `system:identity:bans` role.
|
|
257
|
+
|
|
258
|
+
## Authentication echo
|
|
259
|
+
|
|
260
|
+
Exposition implements a predefined resource `/identity/` with the `GET` method, which returns the
|
|
261
|
+
Identity resolved by the provided credentials.
|
|
262
|
+
|
|
263
|
+
```http
|
|
264
|
+
GET /identity/
|
|
265
|
+
authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
|
|
266
|
+
accept: application/yaml
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
200 OK
|
|
271
|
+
|
|
272
|
+
id: fc8e66ddd51d45eea89602c9dd38a542
|
|
273
|
+
roles:
|
|
274
|
+
- developer
|
|
275
|
+
- system:identity:roles
|
|
276
|
+
```
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Identity
|
|
2
|
+
|
|
3
|
+
Identity is the fundamental entity within an authentication system that represents the **unique
|
|
4
|
+
identifier** of an
|
|
5
|
+
individual, organization, application or device.
|
|
6
|
+
|
|
7
|
+
In order to prove its Identity, the request originator must provide a valid _credentials_ that are
|
|
8
|
+
associated with that
|
|
9
|
+
Identity.
|
|
10
|
+
|
|
11
|
+
Identity is intrinsically linked to credentials, as an Identity is established only when the first
|
|
12
|
+
set of credentials
|
|
13
|
+
for that Identity is created.
|
|
14
|
+
In other words, the creation of credentials marks the inception of an Identity.
|
|
15
|
+
Once the last credentials are removed from the Identity, it ceases to exist.
|
|
16
|
+
Without credentials, there is no basis for defining or asserting an Identity.
|
|
17
|
+
|
|
18
|
+
## Authentication
|
|
19
|
+
|
|
20
|
+
The Authenticaiton system resolves provided credentials to an Identity using one of the supported
|
|
21
|
+
authentication
|
|
22
|
+
schemes.
|
|
23
|
+
|
|
24
|
+
The Authentication is request-agnostic, meaning it does not depend on the specific URL being
|
|
25
|
+
requested or the content of
|
|
26
|
+
the request body.
|
|
27
|
+
The only information it handles is the value of the `Authorization` header.
|
|
28
|
+
|
|
29
|
+
> Except for its own [management resources](#persistent-credentials).
|
|
30
|
+
|
|
31
|
+
If the provided credentials are not valid or not associated with an Identity, then Authentication
|
|
32
|
+
interrupts request
|
|
33
|
+
processing and responds with an authentication error.
|
|
34
|
+
|
|
35
|
+
### Basic scheme
|
|
36
|
+
|
|
37
|
+
Classic username/password pair. See [RFC7617](https://datatracker.ietf.org/doc/html/rfc7617).
|
|
38
|
+
|
|
39
|
+
```http
|
|
40
|
+
Authorization: Basic aGVsbG86d29ybGQK
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
See [`identity.basic` component](components.md#basic-credentials).
|
|
44
|
+
|
|
45
|
+
### Token scheme
|
|
46
|
+
|
|
47
|
+
Tokens issued by the Authentication system. These tokens are [PASETO](https://paseto.io).
|
|
48
|
+
|
|
49
|
+
```http
|
|
50
|
+
Authrization: Token v4.local.eyJzdWIiOiJqb2hu...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The `Token` is the **primary** authentication scheme.
|
|
54
|
+
If request originators use an alternative authentication scheme, they will receive a response
|
|
55
|
+
containing `Token`
|
|
56
|
+
credentials and will be required to switch to the `Token` scheme for any subsequent requests.
|
|
57
|
+
Continued use of other authentication schemes will result in temporary blocking of requests.
|
|
58
|
+
|
|
59
|
+
See [`identity.tokens` component](components.md#stateless-tokens).
|
|
60
|
+
|
|
61
|
+
### Bearer scheme
|
|
62
|
+
|
|
63
|
+
OpenID tokens issued by trusted providers.
|
|
64
|
+
For more information, refer
|
|
65
|
+
to [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html),
|
|
66
|
+
[RFC6750](https://datatracker.ietf.org/doc/html/rfc6750).
|
|
67
|
+
|
|
68
|
+
```http
|
|
69
|
+
Authorization: Bearer eyJhbGciOiJIUzI1...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Trusted providers are specified using the `idenity.trust` property within the Exposition annotation.
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
# context.toa.yaml
|
|
76
|
+
|
|
77
|
+
exposition:
|
|
78
|
+
identity:
|
|
79
|
+
trust:
|
|
80
|
+
- https://accounts.google.com
|
|
81
|
+
- https://appleid.apple.com
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The example above demonstrates the default list of trusted providers.
|
|
85
|
+
|
|
86
|
+
## Identity inception
|
|
87
|
+
|
|
88
|
+
The simplest way to establish a relationship between an Identity and an entity representing a user
|
|
89
|
+
is to synchronize their identifiers.
|
|
90
|
+
|
|
91
|
+
This can be achieved by using the `auth:incept` directive as follows:
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
# manifest.toa.yaml
|
|
95
|
+
name: users
|
|
96
|
+
|
|
97
|
+
entity:
|
|
98
|
+
schema:
|
|
99
|
+
name: string
|
|
100
|
+
|
|
101
|
+
exposition:
|
|
102
|
+
/:
|
|
103
|
+
POST:
|
|
104
|
+
incept: id
|
|
105
|
+
endpoint: transit
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The value of the `auth:incept` directive refers to the name of the response property that will be
|
|
109
|
+
returned by the `POST` operation, containing the created entity identifier.
|
|
110
|
+
|
|
111
|
+
A request with Identity inception must contain (non-existent) credentials that will be associated
|
|
112
|
+
with the created Identity.
|
|
113
|
+
|
|
114
|
+
```http
|
|
115
|
+
POST /users/
|
|
116
|
+
authorization: Basic dXNlcjpwYXNz
|
|
117
|
+
accept: application/yaml
|
|
118
|
+
content-type: application/yaml
|
|
119
|
+
|
|
120
|
+
name: John
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
201 Created
|
|
125
|
+
content-type: application/yaml
|
|
126
|
+
|
|
127
|
+
id: 2428c31ecb6e4a51a24ef52f0c4181b9
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
As a result of processing the above request, the provided Basic credentials associated with the
|
|
131
|
+
Identity `2428c31ecb6e4a51a24ef52f0c4181b9` are created.
|
|
132
|
+
|
|
133
|
+
## FAQ
|
|
134
|
+
|
|
135
|
+
<dl>
|
|
136
|
+
<dt>How can I log in a user?</dt>
|
|
137
|
+
<dd>
|
|
138
|
+
Technically speaking, since the Authentication is request-agnostic, user credentials
|
|
139
|
+
can be sent with any request.
|
|
140
|
+
|
|
141
|
+
However, it is most likely that a request originator will need to obtain an Identity value for
|
|
142
|
+
subsequent requests.
|
|
143
|
+
For this reason, it is recommended to make a `GET /identity/` request.
|
|
144
|
+
</dd>
|
|
145
|
+
<dt>How can I log out a user?</dt>
|
|
146
|
+
<dd>Delete <code>Token</code> credentials from the device.</dd>
|
|
147
|
+
<dt>Where are the sessions?</dt>
|
|
148
|
+
<dd>
|
|
149
|
+
The Authentication is stateless, meaning it does not store any information between
|
|
150
|
+
requests except for persistent credentials.</dd>
|
|
151
|
+
<dt>How can I pass the Identity to an operation call?</dt>
|
|
152
|
+
<dd>
|
|
153
|
+
This is not possible. Refer to the Resource Design Guidelines for more information.
|
|
154
|
+
<a href="https://github.com/toa-io/toa/issues/353">#353</a>
|
|
155
|
+
</dd>
|
|
156
|
+
</dl>
|