@toa.io/extensions.exposition 1.0.0-alpha.101 → 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.
- package/components/identity.basic/manifest.toa.yaml +9 -0
- package/components/identity.basic/operations/authenticate.js +2 -2
- package/components/identity.basic/operations/authenticate.js.map +1 -1
- package/components/identity.basic/operations/incept.js +1 -1
- package/components/identity.basic/operations/incept.js.map +1 -1
- package/components/identity.basic/operations/transit.js +3 -3
- package/components/identity.basic/operations/transit.js.map +1 -1
- package/components/identity.basic/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.basic/source/authenticate.ts +2 -2
- package/components/identity.basic/source/incept.ts +1 -1
- package/components/identity.basic/source/transit.ts +3 -3
- package/components/identity.federation/operations/authenticate.js +1 -1
- package/components/identity.federation/operations/authenticate.js.map +1 -1
- package/components/identity.federation/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.federation/source/authenticate.ts +1 -1
- package/components/identity.keys/manifest.toa.yaml +41 -0
- package/components/identity.keys/operations/create.d.ts +19 -0
- package/components/identity.keys/operations/create.js +14 -0
- package/components/identity.keys/operations/create.js.map +1 -0
- package/components/identity.keys/operations/tsconfig.tsbuildinfo +1 -0
- package/components/identity.keys/source/create.ts +29 -0
- package/components/identity.keys/tsconfig.json +9 -0
- package/components/identity.roles/manifest.toa.yaml +2 -0
- package/components/identity.roles/operations/grant.js +2 -2
- package/components/identity.roles/operations/grant.js.map +1 -1
- package/components/identity.roles/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.roles/source/grant.ts +2 -2
- package/components/identity.tokens/manifest.toa.yaml +41 -9
- package/components/identity.tokens/operations/authenticate.d.ts +2 -2
- package/components/identity.tokens/operations/authenticate.js +12 -12
- package/components/identity.tokens/operations/authenticate.js.map +1 -1
- package/components/identity.tokens/operations/decrypt.d.ts +12 -3
- package/components/identity.tokens/operations/decrypt.js +62 -18
- package/components/identity.tokens/operations/decrypt.js.map +1 -1
- package/components/identity.tokens/operations/encrypt.d.ts +3 -3
- package/components/identity.tokens/operations/encrypt.js +23 -7
- package/components/identity.tokens/operations/encrypt.js.map +1 -1
- package/components/identity.tokens/operations/issue.d.ts +18 -0
- package/components/identity.tokens/operations/issue.js +44 -0
- package/components/identity.tokens/operations/issue.js.map +1 -0
- package/components/identity.tokens/operations/lib/index.d.ts +2 -0
- package/components/identity.tokens/operations/lib/index.js +19 -0
- package/components/identity.tokens/operations/lib/index.js.map +1 -0
- package/components/identity.tokens/operations/lib/pad.d.ts +1 -0
- package/components/identity.tokens/operations/lib/pad.js +5 -0
- package/components/identity.tokens/operations/lib/pad.js.map +1 -0
- package/components/identity.tokens/operations/{types.d.ts → lib/types.d.ts} +31 -7
- package/components/identity.tokens/operations/lib/types.js.map +1 -0
- package/components/identity.tokens/operations/revoke.d.ts +2 -2
- package/components/identity.tokens/operations/revoke.js.map +1 -1
- package/components/identity.tokens/operations/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/source/authenticate.test.ts +10 -7
- package/components/identity.tokens/source/authenticate.ts +14 -14
- package/components/identity.tokens/source/decrypt.test.ts +26 -16
- package/components/identity.tokens/source/decrypt.ts +90 -20
- package/components/identity.tokens/source/encrypt.test.ts +41 -13
- package/components/identity.tokens/source/encrypt.ts +35 -11
- package/components/identity.tokens/source/issue.ts +57 -0
- package/components/identity.tokens/source/lib/index.ts +2 -0
- package/components/identity.tokens/source/lib/pad.ts +1 -0
- package/components/identity.tokens/source/lib/paseto.test.ts +16 -0
- package/components/identity.tokens/source/{types.ts → lib/types.ts} +33 -7
- package/components/identity.tokens/source/revoke.ts +2 -2
- package/components/octets.storage/operations/put.js +4 -4
- package/documentation/components.md +77 -39
- package/features/identity.tokens.feature +0 -43
- package/features/identtiy.tokens.custom.feature +236 -0
- package/features/octets.cloudinary.feature +2 -2
- package/features/steps/Gateway.ts +3 -1
- package/package.json +7 -4
- package/source/directives/auth/Authorization.ts +30 -18
- package/source/directives/auth/Delegate.ts +1 -3
- package/source/directives/auth/Role.ts +4 -8
- package/source/directives/auth/types.ts +2 -1
- package/source/directives/octets/Put.ts +3 -19
- package/transpiled/directives/auth/Authorization.d.ts +2 -1
- package/transpiled/directives/auth/Authorization.js +25 -16
- package/transpiled/directives/auth/Authorization.js.map +1 -1
- package/transpiled/directives/auth/Delegate.js +1 -2
- package/transpiled/directives/auth/Delegate.js.map +1 -1
- package/transpiled/directives/auth/Role.d.ts +1 -1
- package/transpiled/directives/auth/Role.js +3 -5
- package/transpiled/directives/auth/Role.js.map +1 -1
- package/transpiled/directives/auth/types.d.ts +2 -1
- package/transpiled/directives/octets/Put.d.ts +0 -1
- package/transpiled/directives/octets/Put.js +0 -11
- package/transpiled/directives/octets/Put.js.map +1 -1
- package/transpiled/tsconfig.tsbuildinfo +1 -1
- package/components/identity.tokens/operations/types.js.map +0 -1
- /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 {
|
|
6
|
-
import { type Context, type Identity } from './
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
35
|
-
throw
|
|
59
|
+
if (encrypted instanceof Error)
|
|
60
|
+
throw encrypted
|
|
36
61
|
|
|
37
|
-
await expect(decrypt(encrypted
|
|
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
|
|
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
|
-
|
|
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 {
|
|
3
|
-
import
|
|
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
|
|
7
|
+
private key!: Key
|
|
7
8
|
private lifetime: number = 0
|
|
8
9
|
|
|
9
10
|
public mount (context: Context): void {
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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 @@
|
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
|
67
|
+
export interface Claims {
|
|
51
68
|
identity: Identity
|
|
52
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
##
|
|
125
|
+
## Local tokens
|
|
126
126
|
|
|
127
|
-
The `identity.tokens` component manages
|
|
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 `
|
|
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
|
-
|
|
219
|
+
keys:
|
|
220
|
+
2024q1: $TOKEN_SECRET_2024Q1
|
|
158
221
|
```
|
|
159
222
|
|
|
160
|
-
|
|
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 `
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
@@ -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
|
-
"""
|