@toa.io/extensions.exposition 1.0.0-alpha.100 → 1.0.0-alpha.102

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.
Files changed (93) hide show
  1. package/components/identity.basic/manifest.toa.yaml +9 -0
  2. package/components/identity.basic/operations/authenticate.js +2 -2
  3. package/components/identity.basic/operations/authenticate.js.map +1 -1
  4. package/components/identity.basic/operations/incept.js +1 -1
  5. package/components/identity.basic/operations/incept.js.map +1 -1
  6. package/components/identity.basic/operations/transit.js +3 -3
  7. package/components/identity.basic/operations/transit.js.map +1 -1
  8. package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
  9. package/components/identity.basic/source/authenticate.ts +2 -2
  10. package/components/identity.basic/source/incept.ts +1 -1
  11. package/components/identity.basic/source/transit.ts +3 -3
  12. package/components/identity.federation/manifest.toa.yaml +20 -6
  13. package/components/identity.federation/operations/authenticate.js +1 -1
  14. package/components/identity.federation/operations/authenticate.js.map +1 -1
  15. package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
  16. package/components/identity.federation/source/authenticate.ts +1 -1
  17. package/components/identity.keys/manifest.toa.yaml +41 -0
  18. package/components/identity.keys/operations/create.d.ts +19 -0
  19. package/components/identity.keys/operations/create.js +14 -0
  20. package/components/identity.keys/operations/create.js.map +1 -0
  21. package/components/identity.keys/operations/tsconfig.tsbuildinfo +1 -0
  22. package/components/identity.keys/source/create.ts +29 -0
  23. package/components/identity.keys/tsconfig.json +9 -0
  24. package/components/identity.roles/manifest.toa.yaml +2 -0
  25. package/components/identity.roles/operations/grant.js +2 -2
  26. package/components/identity.roles/operations/grant.js.map +1 -1
  27. package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
  28. package/components/identity.roles/source/grant.ts +2 -2
  29. package/components/identity.tokens/manifest.toa.yaml +41 -9
  30. package/components/identity.tokens/operations/authenticate.d.ts +2 -2
  31. package/components/identity.tokens/operations/authenticate.js +12 -12
  32. package/components/identity.tokens/operations/authenticate.js.map +1 -1
  33. package/components/identity.tokens/operations/decrypt.d.ts +12 -3
  34. package/components/identity.tokens/operations/decrypt.js +62 -18
  35. package/components/identity.tokens/operations/decrypt.js.map +1 -1
  36. package/components/identity.tokens/operations/encrypt.d.ts +3 -3
  37. package/components/identity.tokens/operations/encrypt.js +23 -7
  38. package/components/identity.tokens/operations/encrypt.js.map +1 -1
  39. package/components/identity.tokens/operations/issue.d.ts +18 -0
  40. package/components/identity.tokens/operations/issue.js +44 -0
  41. package/components/identity.tokens/operations/issue.js.map +1 -0
  42. package/components/identity.tokens/operations/lib/index.d.ts +2 -0
  43. package/components/identity.tokens/operations/lib/index.js +19 -0
  44. package/components/identity.tokens/operations/lib/index.js.map +1 -0
  45. package/components/identity.tokens/operations/lib/pad.d.ts +1 -0
  46. package/components/identity.tokens/operations/lib/pad.js +5 -0
  47. package/components/identity.tokens/operations/lib/pad.js.map +1 -0
  48. package/components/identity.tokens/operations/{types.d.ts → lib/types.d.ts} +31 -7
  49. package/components/identity.tokens/operations/lib/types.js.map +1 -0
  50. package/components/identity.tokens/operations/revoke.d.ts +2 -2
  51. package/components/identity.tokens/operations/revoke.js.map +1 -1
  52. package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
  53. package/components/identity.tokens/source/authenticate.test.ts +10 -7
  54. package/components/identity.tokens/source/authenticate.ts +14 -14
  55. package/components/identity.tokens/source/decrypt.test.ts +26 -16
  56. package/components/identity.tokens/source/decrypt.ts +90 -20
  57. package/components/identity.tokens/source/encrypt.test.ts +41 -13
  58. package/components/identity.tokens/source/encrypt.ts +35 -11
  59. package/components/identity.tokens/source/issue.ts +57 -0
  60. package/components/identity.tokens/source/lib/index.ts +2 -0
  61. package/components/identity.tokens/source/lib/pad.ts +1 -0
  62. package/components/identity.tokens/source/lib/paseto.test.ts +16 -0
  63. package/components/identity.tokens/source/{types.ts → lib/types.ts} +33 -7
  64. package/components/identity.tokens/source/revoke.ts +2 -2
  65. package/components/octets.storage/operations/put.js +4 -4
  66. package/documentation/components.md +77 -39
  67. package/features/auth.claims.feature +1 -0
  68. package/features/identity.basic.feature +2 -2
  69. package/features/identity.tokens.feature +0 -43
  70. package/features/identtiy.tokens.custom.feature +236 -0
  71. package/features/octets.cloudinary.feature +2 -2
  72. package/features/steps/Gateway.ts +3 -1
  73. package/package.json +7 -4
  74. package/source/directives/auth/Authorization.ts +30 -18
  75. package/source/directives/auth/Delegate.ts +1 -3
  76. package/source/directives/auth/Role.ts +4 -8
  77. package/source/directives/auth/types.ts +2 -1
  78. package/source/directives/octets/Put.ts +3 -19
  79. package/transpiled/directives/auth/Authorization.d.ts +2 -1
  80. package/transpiled/directives/auth/Authorization.js +25 -16
  81. package/transpiled/directives/auth/Authorization.js.map +1 -1
  82. package/transpiled/directives/auth/Delegate.js +1 -2
  83. package/transpiled/directives/auth/Delegate.js.map +1 -1
  84. package/transpiled/directives/auth/Role.d.ts +1 -1
  85. package/transpiled/directives/auth/Role.js +3 -5
  86. package/transpiled/directives/auth/Role.js.map +1 -1
  87. package/transpiled/directives/auth/types.d.ts +2 -1
  88. package/transpiled/directives/octets/Put.d.ts +0 -1
  89. package/transpiled/directives/octets/Put.js +0 -11
  90. package/transpiled/directives/octets/Put.js.map +1 -1
  91. package/transpiled/tsconfig.tsbuildinfo +1 -1
  92. package/components/identity.tokens/operations/types.js.map +0 -1
  93. /package/components/identity.tokens/operations/{types.js → lib/types.js} +0 -0
@@ -2,27 +2,52 @@ import assert from 'node:assert'
2
2
  import { generate } from 'randomstring'
3
3
  import { timeout } from '@toa.io/generic'
4
4
  import { Effect as Encrypt } from './encrypt'
5
- import { computation as decrypt } from './decrypt'
6
- import { type Context, type Identity } from './types'
5
+ import { Computation as Decrypt } from './decrypt'
6
+ import { type Context, type Identity } from './lib'
7
7
 
8
8
  let encrypt: Encrypt
9
+ let decrypt: Decrypt
9
10
 
10
- const context: Context = {} as unknown as Context
11
+ const context: Context = { remote: { identity: { keys: null } } } as unknown as Context
11
12
  const authority = generate()
12
13
 
13
14
  beforeEach(() => {
14
15
  context.configuration = {
15
- key0: 'k3.local.m28p8SrbS467t-2IUjQuSOqmjvi24TbXhyjAW_dOrog',
16
- lifetime: 1000,
17
- refresh: 2
16
+ keys: {
17
+ key0: 'k3.local.m28p8SrbS467t-2IUjQuSOqmjvi24TbXhyjAW_dOrog'
18
+ },
19
+ lifetime: 1,
20
+ refresh: 2,
21
+ cache: 3
18
22
  }
19
23
 
20
24
  encrypt = new Encrypt()
21
25
  encrypt.mount(context)
26
+
27
+ decrypt = new Decrypt()
28
+ decrypt.mount(context)
29
+ })
30
+
31
+ it('should encrypt with configured lifetime by default', async () => {
32
+ const identity: Identity = { id: generate(), roles: [] }
33
+
34
+ const encrypted = await encrypt.execute({
35
+ authority,
36
+ identity
37
+ })
38
+
39
+ if (encrypted instanceof Error)
40
+ throw encrypted
41
+
42
+ await expect(decrypt.execute(encrypted)).resolves.toMatchObject({ iss: authority, identity })
43
+
44
+ await timeout(context.configuration.lifetime * 1000)
45
+
46
+ await expect(decrypt.execute(encrypted)).resolves.toMatchObject({ code: 'INVALID_TOKEN' })
22
47
  })
23
48
 
24
49
  it('should encrypt with given lifetime', async () => {
25
- const identity: Identity = { id: generate() }
50
+ const identity: Identity = { id: generate(), roles: [] }
26
51
  const lifetime = 0.1
27
52
 
28
53
  const encrypted = await encrypt.execute({
@@ -31,18 +56,18 @@ it('should encrypt with given lifetime', async () => {
31
56
  lifetime
32
57
  })
33
58
 
34
- if (encrypted === undefined)
35
- throw new Error('?')
59
+ if (encrypted instanceof Error)
60
+ throw encrypted
36
61
 
37
- await expect(decrypt(encrypted, context)).resolves.toMatchObject({ authority, identity })
62
+ await expect(decrypt.execute(encrypted)).resolves.toMatchObject({ iss: authority, identity })
38
63
 
39
64
  await timeout(lifetime * 1000)
40
65
 
41
- await expect(decrypt(encrypted, context)).resolves.toMatchObject({ message: 'INVALID_TOKEN' })
66
+ await expect(decrypt.execute(encrypted)).resolves.toMatchObject({ code: 'INVALID_TOKEN' })
42
67
  })
43
68
 
44
69
  it('should encrypt without lifetime INSECURE', async () => {
45
- const identity: Identity = { id: generate() }
70
+ const identity: Identity = { id: generate(), roles: [] }
46
71
  const lifetime = 0
47
72
 
48
73
  const encrypted = await encrypt.execute({
@@ -51,7 +76,10 @@ it('should encrypt without lifetime INSECURE', async () => {
51
76
  lifetime
52
77
  })
53
78
 
54
- const decrypted = await decrypt(encrypted, context)
79
+ if (encrypted instanceof Error)
80
+ throw encrypted
81
+
82
+ const decrypted = await decrypt.execute(encrypted)
55
83
 
56
84
  assert.ok(!(decrypted instanceof Error))
57
85
 
@@ -1,29 +1,53 @@
1
1
  import { V3 } from 'paseto'
2
- import { type Operation } from '@toa.io/types'
3
- import { type Claim, type Context, type EncryptInput } from './types'
2
+ import { Err } from 'error-value'
3
+ import type { Operation, Maybe } from '@toa.io/types'
4
+ import type { Identity, Claims, Context, EncryptInput, Key } from './lib'
4
5
 
5
6
  export class Effect implements Operation {
6
- private key: string = ''
7
+ private key!: Key
7
8
  private lifetime: number = 0
8
9
 
9
10
  public mount (context: Context): void {
10
- this.key = context.configuration.key0
11
+ const [id, secret] = Object.entries(context.configuration.keys)[0]
12
+
13
+ this.key = { id, key: secret }
11
14
  this.lifetime = context.configuration.lifetime * 1000
12
15
  }
13
16
 
14
- public async execute (input: EncryptInput): Promise<string> {
15
- const lifetime = input.lifetime === undefined ? this.lifetime : input.lifetime * 1000
17
+ public async execute (input: EncryptInput): Promise<Maybe<string>> {
18
+ if (input.scopes?.some((scope) => !within(scope, input.identity.roles)) === true)
19
+ return ERR_INACCESSIBLE_SCOPE
20
+
21
+ const lifetime = input.lifetime === undefined ? this.lifetime : (input.lifetime * 1000)
16
22
 
17
23
  const exp = lifetime === 0
18
24
  ? undefined
19
25
  : new Date(Date.now() + lifetime).toISOString()
20
26
 
21
- const payload: Partial<Claim> = {
22
- identity: input.identity,
23
- aud: input.authority,
24
- exp
27
+ const identity: Identity = {
28
+ id: input.identity.id,
29
+ roles: input.scopes ?? input.identity.roles
30
+ }
31
+
32
+ if (input.permissions !== undefined)
33
+ identity.permissions = input.permissions
34
+
35
+ const payload: Partial<Claims> = {
36
+ identity,
37
+ iss: input.authority
25
38
  }
26
39
 
27
- return await V3.encrypt(payload, this.key)
40
+ if (exp !== undefined)
41
+ payload.exp = exp
42
+
43
+ const key = input.key ?? this.key
44
+
45
+ return await V3.encrypt(payload, key.key, { footer: { kid: key.id } })
28
46
  }
29
47
  }
48
+
49
+ function within (scope: string, roles: string[]): boolean {
50
+ return roles.some((role) => role === scope || scope.startsWith(role + ':'))
51
+ }
52
+
53
+ const ERR_INACCESSIBLE_SCOPE = new Err('INACCESSIBLE_SCOPE')
@@ -0,0 +1,57 @@
1
+ import type { Maybe, Operation } from '@toa.io/types'
2
+ import type { Context, Identity } from './lib'
3
+
4
+ export class Effect implements Operation {
5
+ private keys!: Context['remote']['identity']['keys']
6
+ private roles!: Context['remote']['identity']['roles']
7
+ private encrypt!: Context['local']['encrypt']
8
+
9
+ public mount (context: Context): void {
10
+ this.keys = context.remote.identity.keys
11
+ this.roles = context.remote.identity.roles
12
+ this.encrypt = context.local.encrypt
13
+ }
14
+
15
+ public async execute (input: Input): Promise<Maybe<string>> {
16
+ const key = await this.keys.create({
17
+ input: {
18
+ identity: input.identity,
19
+ name: input.name
20
+ }
21
+ })
22
+
23
+ const roles = await this.roles.list({
24
+ query: {
25
+ criteria: `identity==${input.identity}`,
26
+ limit: 1024
27
+ }
28
+ })
29
+
30
+ const identity: Identity = {
31
+ id: input.identity,
32
+ roles
33
+ }
34
+
35
+ const { authority, lifetime, scopes, permissions } = input
36
+
37
+ return await this.encrypt({
38
+ input: {
39
+ authority,
40
+ identity,
41
+ lifetime,
42
+ scopes,
43
+ permissions,
44
+ key
45
+ }
46
+ })
47
+ }
48
+ }
49
+
50
+ interface Input {
51
+ authority: string
52
+ identity: string
53
+ lifetime?: number
54
+ scopes?: string[]
55
+ permissions?: Record<string, string[]>
56
+ name?: string
57
+ }
@@ -0,0 +1,2 @@
1
+ export * from './pad'
2
+ export * from './types'
@@ -0,0 +1 @@
1
+ export const PAD = 'v3.local.'
@@ -0,0 +1,16 @@
1
+ import { V3 } from 'paseto'
2
+
3
+ it('should be ok', async () => {
4
+ const payload = { iss: 'test', sub: 'me' }
5
+ const key = await V3.generateKey('local', { format: 'paserk' })
6
+ const token = await V3.encrypt(payload, key, { footer: 'key0' })
7
+ const [, , , footer] = token.split('.')
8
+ const kid = Buffer.from(footer, 'base64url').toString('utf-8')
9
+
10
+ console.log(footer, kid)
11
+ console.log(key)
12
+
13
+ const decrypted = await V3.decrypt(token, key)
14
+
15
+ console.log(decrypted)
16
+ })
@@ -1,18 +1,30 @@
1
- import { type Call, type Maybe, type Observation } from '@toa.io/types'
1
+ import type { Call, Maybe, Observation } from '@toa.io/types'
2
2
 
3
3
  export interface Context {
4
4
  local: {
5
5
  observe: Observation<Entity>
6
+ encrypt: Call<Maybe<string>, EncryptInput>
6
7
  decrypt: Call<Maybe<DecryptOutput>, string>
7
8
  }
9
+ remote: {
10
+ identity: {
11
+ keys: {
12
+ observe: Observation<CustomKey>
13
+ create: Call<Key>
14
+ }
15
+ roles: {
16
+ list: Call<string[]>
17
+ }
18
+ }
19
+ }
8
20
  configuration: Configuration
9
21
  }
10
22
 
11
23
  export interface Configuration {
12
- readonly key0: string
13
- readonly key1?: string
24
+ readonly keys: Record<string, string>
14
25
  readonly lifetime: number
15
26
  readonly refresh: number
27
+ readonly cache: number
16
28
  }
17
29
 
18
30
  export interface Entity {
@@ -21,6 +33,8 @@ export interface Entity {
21
33
 
22
34
  export interface Identity extends Record<string, any> {
23
35
  id: string
36
+ roles: string[]
37
+ permissions?: Record<string, string[]>
24
38
  }
25
39
 
26
40
  export interface AuthenticateInput {
@@ -37,19 +51,31 @@ export interface EncryptInput {
37
51
  authority: string
38
52
  identity: Identity
39
53
  lifetime?: number
54
+ scopes?: string[]
55
+ permissions?: Record<string, string[]>
56
+ key?: Key
40
57
  }
41
58
 
42
59
  export interface DecryptOutput {
43
- authority: string
44
- identity: Identity
60
+ iss: string
45
61
  iat: string
46
62
  exp?: string
63
+ identity: Identity
47
64
  refresh: boolean
48
65
  }
49
66
 
50
- export interface Claim {
67
+ export interface Claims {
51
68
  identity: Identity
52
- aud: string
69
+ iss: string
53
70
  iat: string
54
71
  exp?: string
55
72
  }
73
+
74
+ export interface Key {
75
+ id: string
76
+ key: string
77
+ }
78
+
79
+ export interface CustomKey extends Key {
80
+ identity: string
81
+ }
@@ -1,5 +1,5 @@
1
- import { type Entity } from './types'
1
+ import type { Entity } from './lib'
2
2
 
3
- export function transition (_: never, object: Entity): void {
3
+ export function transition (_: unknown, object: Entity): void {
4
4
  object.revokedAt = Date.now()
5
5
  }
@@ -119,10 +119,10 @@ function toURL (location) {
119
119
  }
120
120
  }
121
121
 
122
- const ERR_UNTRUSTED = Err('LOCATION_UNTRUSTED', 'Location is not trusted')
123
- const ERR_LENGTH = Err('LOCATION_LENGTH', 'Content-Length must be 0 when Content-Location is used')
124
- const ERR_UNAVAILABLE = Err('LOCATION_UNAVAILABLE', 'Location is not available')
125
- const ERR_INVALID_ID = Err('INVALID_ID', 'Invalid Content-ID')
122
+ const ERR_UNTRUSTED = new Err('LOCATION_UNTRUSTED', 'Location is not trusted')
123
+ const ERR_LENGTH = new Err('LOCATION_LENGTH', 'Content-Length must be 0 when Content-Location is used')
124
+ const ERR_UNAVAILABLE = new Err('LOCATION_UNAVAILABLE', 'Location is not available')
125
+ const ERR_INVALID_ID = new Err('INVALID_ID', 'Invalid Content-ID')
126
126
 
127
127
  const ID_RX = /^[a-zA-Z0-9-_]{1,32}$/
128
128
 
@@ -122,9 +122,9 @@ configuration:
122
122
  k1: <secret-to-be-used-for-hs256>
123
123
  ```
124
124
 
125
- ## Stateless tokens
125
+ ## Local tokens
126
126
 
127
- The `identity.tokens` component manages stateless authentication tokens.
127
+ The `identity.tokens` component manages local authentication tokens.
128
128
 
129
129
  These tokens carry the information required to authenticate the Identity and authorize access.
130
130
 
@@ -143,21 +143,84 @@ authorization: Token ...
143
143
  cache-control: no-store
144
144
  ```
145
145
 
146
+ ### Custom tokens
147
+
148
+ Custom tokens can be issued with a specific set of permissions and scopes for the own Identity or by
149
+ an Identity with the `system:identity:tokens` role.
150
+
151
+ Tokens are issued with custom secret keys and are not subject to [token rotation](#token-rotation).
152
+ To invalidate a custom token, its secret key must be deleted.
153
+
154
+ Custom tokens have no `refresh` period, that is, never become obsolete and never refreshed.
155
+
156
+ ```
157
+ POST /identity/tokens/<identity>/
158
+ host: nex.toa.io
159
+ authorization: ...
160
+ accept: application/yaml
161
+ content-type: application/yaml
162
+
163
+ lifetime: 3600
164
+ scopes: [app:developer]
165
+ permissions:
166
+ /users/fc8e66dd/: [GET, PUT]
167
+ /posts/fc8e66dd/**/comments/: [*]
168
+ ```
169
+
170
+ ```
171
+ 201 Created
172
+ content-type: application/yaml
173
+
174
+ token: <token>
175
+ ```
176
+
177
+ - `lifetime`: Issued token will be valid for this period
178
+ (default is specified in [the configuration](#token-rotation)).
179
+ The value of `0` means the token will not expire, which is supported, but
180
+ **strongly not recommended** for production environments.
181
+ - `scopes`: Issued token will assume only specified [role scopes](access.md#roles).
182
+ - `permissions`: Issued token will have permissions to access only specified resources and methods.
183
+ Supports [glob patterns](https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html)
184
+ and a wildcard method.
185
+
186
+ > `roles` and `permissions` are additional restrictions applied on top of the Identity’s inherent
187
+ > privileges.
188
+
189
+ ### Custom token invalidation
190
+
191
+ Custom tokens can be invalidated by deleting the secret key used to issue them.
192
+ This can be done by the Identity that issued the token or by an Identity with
193
+ the `system:identity:keys` role.
194
+
195
+ ```
196
+ DELETE /identity/keys/<identity>/<key.id>/
197
+ authorization: ...
198
+ ```
199
+
200
+ Token secret key `id` can be obtained from the list of issued tokens (or from the footer of the
201
+ token itself).
202
+
203
+ ```
204
+ GET /identity/keys/<identity>/
205
+ authorization: ...
206
+ ```
207
+
146
208
  ### Token encryption
147
209
 
148
210
  Issued tokens are encrypted
149
211
  with [PASETO V3 encryption](https://github.com/panva/paseto/blob/main/docs/README.md#v3encryptpayload-key-options)
150
- using the `key0` configuration value as a secret.
212
+ using the first key from the `keys` configuration value.
151
213
 
152
214
  ```yaml
153
215
  # context.toa.yaml
154
216
 
155
217
  configuration:
156
218
  identity.tokens:
157
- key0: $TOKEN_ENCRYPTION_KEY
219
+ keys:
220
+ 2024q1: $TOKEN_SECRET_2024Q1
158
221
  ```
159
222
 
160
- The `key0` configuration value is required.
223
+ At least one key in the `keys` configuration value is required.
161
224
 
162
225
  > Valid secret key may be generated using the [`toa key` command](/runtime/cli/readme.md#key).
163
226
 
@@ -197,43 +260,18 @@ Token revocation takes effect once the `refresh` period of the currently issued
197
260
 
198
261
  ### Secret rotation
199
262
 
200
- Tokens are always encrypted using the `key0` configuration value, and they will be decrypted by
201
- attempting both
202
- the `key0` and `key1` values in order.
203
-
204
- `key0` is considered the "current key," and `key1` is considered the "previous key."
205
-
206
- ```yaml
207
- # context.toa.yaml
208
-
209
- configuration:
210
- identity.tokens:
211
- key0: $TOKEN_ENCRYPTION_KEY_2023Q3
212
- key1: $TOKEN_ENCRYPTION_KEY_2023Q2
213
- ```
214
-
215
- Secret rotation is performed by adding a new key as the `key0` value and moving the existing `key0`
216
- to the `key1` value.
217
-
218
- When rolling out the new secret key, there will be a period of time when the new key is deployed to
219
- some Exposition
220
- instances. During this time, these instances will start using the new key to encrypt tokens, while
221
- other instances will
222
- continue using the current key and will not be able to decrypt tokens encrypted with the new key.
223
-
224
- To address this issue, the `key1` configuration value may be used as a "transient key."
263
+ Tokens are always encrypted using the first key from the `keys` configuration value,
264
+ and decrypted by the key used to encrypt them.
225
265
 
226
- The secret rotation is a 2-step process:
266
+ To rotate the secret key, a new key must be added to the top of the `keys` configuration value, that
267
+ is, it will be used to encrypt new tokens.
227
268
 
228
- > The process **must not** be performed earlier than the `lifetime` period since the last rotation,
229
- > as it may invalidate
230
- > tokens before they expire. Therefore, it is guaranteed that there are no valid tokens issued with
231
- > the current `key1`
232
- > value.
269
+ Old keys must be removed only after the `refresh` period of the previously issued tokens has
270
+ expired.
233
271
 
234
- 1. Deploy the new secret key to all Exposition instances as `key1`. This enables all instances to
235
- decrypt tokens
236
- encrypted with the new key while still using the current key for encryption.
272
+ > Let's say you are adding a new secret key each quarter: `2024Q1`, `2024Q2` and so on.
273
+ > The old key `2024Q1` must be removed from the configuration only when the `refresh` period after
274
+ > the new key `2024Q2` was added has expired.
237
275
 
238
276
  ```yaml
239
277
  # context.toa.yaml
@@ -9,6 +9,7 @@ Feature: Federated identity authentication
9
9
  """yaml
10
10
  trust:
11
11
  - iss: http://localhost:44444
12
+ implicit: true
12
13
  """
13
14
 
14
15
  Scenario: Full claim
@@ -117,7 +117,7 @@ Feature: Basic authentication
117
117
  """
118
118
  Then the following reply is sent:
119
119
  """
120
- 409 Conflict
120
+ 403 Forbidden
121
121
  """
122
122
 
123
123
  Scenario: Changing the password
@@ -360,7 +360,7 @@ Feature: Basic authentication
360
360
  """
361
361
  Then the following reply is sent:
362
362
  """
363
- 409 Conflict
363
+ 403 Forbidden
364
364
  """
365
365
 
366
366
  Scenario: Incorrect credentials format
@@ -127,46 +127,3 @@ Feature: Tokens lifecycle
127
127
  """
128
128
  401 Unauthorized
129
129
  """
130
-
131
- Scenario: Issuing own token
132
- Given the `identity.basic` database contains:
133
- | _id | authority | username | password |
134
- | efe3a65ebbee47ed95a73edd911ea328 | nex | developer | $2b$10$ZRSKkgZoGnrcTNA5w5eCcu3pxDzdTduhteVYXcp56AaNcilNkwJ.O |
135
- When the following request is received:
136
- """
137
- GET /identity/ HTTP/1.1
138
- host: nex.toa.io
139
- authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
140
- """
141
- Then the following reply is sent:
142
- """
143
- 200 OK
144
- authorization: Token ${{ token }}
145
- """
146
- When the following request is received:
147
- """
148
- POST /identity/tokens/ HTTP/1.1
149
- host: nex.toa.io
150
- authorization: Token ${{ token }}
151
- content-type: application/yaml
152
-
153
- lifetime: 0
154
- """
155
- Then the following reply is sent:
156
- """
157
- 201 Created
158
- """
159
- # Token scheme must be used
160
- When the following request is received:
161
- """
162
- POST /identity/tokens/ HTTP/1.1
163
- host: nex.toa.io
164
- authorization: Basic ZGV2ZWxvcGVyOnNlY3JldA==
165
- content-type: application/yaml
166
-
167
- lifetime: 60
168
- """
169
- Then the following reply is sent:
170
- """
171
- 403 Forbidden
172
- """