@toa.io/extensions.exposition 1.0.0-alpha.130 → 1.0.0-alpha.132

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.
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto'
2
+ import { newid } from '@toa.io/generic'
2
3
  import { MAX_KEYS } from './lib/const'
3
4
  import type { Operation } from '@toa.io/types'
4
5
  import type { Context } from './types'
@@ -31,7 +32,7 @@ export class Effect implements Operation {
31
32
  }
32
33
 
33
34
  public async execute (input: Input): Promise<Output> {
34
- const { identity, authority } = input
35
+ const { type, identity, authority } = input
35
36
  const challenge = await this.createChallenge(authority)
36
37
 
37
38
  const options: Output = {
@@ -39,23 +40,31 @@ export class Effect implements Operation {
39
40
  timeout: this.timeout
40
41
  }
41
42
 
42
- if (identity === undefined)
43
- return options
44
-
45
- const keys = await this.enumerate({
46
- query: {
47
- criteria: `identity==${identity}`,
48
- projection: ['kid', 'transports'],
49
- limit: MAX_KEYS
50
- }
51
- })
52
-
53
- return {
54
- ...options,
55
- excludeCredentials: keys.map(({ kid, transports }) => ({ id: kid, transports })),
56
- pubKeyCredParams: this.credParams,
57
- authenticatorSelection: this.authenticator
58
- }
43
+ if (type === 'creation')
44
+ options.identity = identity ?? newid()
45
+
46
+ const keys = identity === undefined
47
+ ? undefined
48
+ : await this.enumerate({
49
+ query: {
50
+ criteria: `identity==${identity}`,
51
+ projection: ['kid', 'transports'],
52
+ limit: MAX_KEYS
53
+ }
54
+ })
55
+
56
+ if (type === 'creation')
57
+ return {
58
+ ...options,
59
+ excludeCredentials: keys?.map(({ kid, transports }) => ({ id: kid, transports })),
60
+ pubKeyCredParams: this.credParams,
61
+ authenticatorSelection: this.authenticator
62
+ } satisfies CreationOptions
63
+ else
64
+ return {
65
+ ...options,
66
+ allowCredentials: keys?.map(({ kid, transports }) => ({ id: kid, transports }))
67
+ } satisfies RequestOptions
59
68
  }
60
69
 
61
70
  private async createChallenge (authority: string): Promise<string> {
@@ -73,6 +82,7 @@ export class Effect implements Operation {
73
82
  const EX_GAP = 1.5
74
83
 
75
84
  interface Input {
85
+ type: 'creation' | 'request'
76
86
  authority: string
77
87
  identity?: string | null
78
88
  }
@@ -80,6 +90,7 @@ interface Input {
80
90
  type Output = CreationOptions | RequestOptions
81
91
 
82
92
  interface CommonOptions {
93
+ identity?: string
83
94
  challenge: string
84
95
  timeout: number
85
96
  }
@@ -91,7 +102,10 @@ interface KeyDescriptor {
91
102
 
92
103
  type CreationOptions =
93
104
  CommonOptions
94
- & Omit<PublicKeyCredentialCreationOptions, 'rp' | 'user' | 'excludeCredentials'>
95
- & { excludeCredentials: KeyDescriptor[] }
105
+ & Omit<PublicKeyCredentialCreationOptions, 'challenge' | 'rp' | 'user' | 'excludeCredentials'>
106
+ & { excludeCredentials?: KeyDescriptor[] }
96
107
 
97
- type RequestOptions = CommonOptions
108
+ type RequestOptions =
109
+ CommonOptions
110
+ & Omit<PublicKeyCredentialRequestOptions, 'challenge' | 'allowCredentials'>
111
+ & { allowCredentials?: KeyDescriptor[] }
@@ -9,9 +9,13 @@ export class Transition implements Operation {
9
9
  private algorithms!: number[]
10
10
  private stash!: Context['stash']
11
11
  private logs!: Context['logs']
12
+ private verification!: boolean
13
+ private presence!: boolean
12
14
 
13
15
  public mount (context: Context): void {
14
16
  this.algorithms = context.configuration.algorithms
17
+ this.verification = context.configuration.verification === 'required'
18
+ this.presence = context.configuration.residence === 'required'
15
19
  this.stash = context.stash
16
20
  this.logs = context.logs
17
21
  }
@@ -26,7 +30,9 @@ export class Transition implements Operation {
26
30
  response,
27
31
  expectedOrigin: input.origin,
28
32
  expectedChallenge: async (challenge) => this.verifyChallenge(authority, challenge),
29
- supportedAlgorithmIDs: this.algorithms
33
+ supportedAlgorithmIDs: this.algorithms,
34
+ requireUserVerification: this.verification,
35
+ requireUserPresence: this.presence
30
36
  }).catch((e) => {
31
37
  this.logs.info('Failed to verify registration response', { message: e.message })
32
38
 
@@ -7,10 +7,14 @@ import type { Context, Passkey } from './types'
7
7
  export class Transition implements Operation {
8
8
  private stash!: Context['stash']
9
9
  private logs!: Context['logs']
10
+ private verification!: boolean
11
+ private presence!: boolean
10
12
 
11
13
  public mount (context: Context): void {
12
14
  this.stash = context.stash
13
15
  this.logs = context.logs
16
+ this.verification = context.configuration.verification === 'required'
17
+ this.presence = context.configuration.residence === 'required'
14
18
  }
15
19
 
16
20
  public async execute (input: Input, object: Passkey): Promise<Output> {
@@ -23,7 +27,8 @@ export class Transition implements Operation {
23
27
  credential,
24
28
  expectedOrigin: origin,
25
29
  expectedRPID: new URL(origin).hostname,
26
- expectedChallenge: async (challenge) => this.verifyChallenge(object.authority, challenge)
30
+ expectedChallenge: async (challenge) => this.verifyChallenge(object.authority, challenge),
31
+ requireUserVerification: this.verification
27
32
  }).catch((e) => {
28
33
  this.logs.info('Failed to verify authentication response', { message: e.message })
29
34
 
@@ -12,12 +12,46 @@ Feature: Web Authentication
12
12
  host: nex.toa.io
13
13
  authorization: Token ${{ identity.token }}
14
14
  accept: application/yaml
15
+ content-type: application/yaml
16
+
17
+ type: creation
18
+ """
19
+ Then the following reply is sent:
20
+ """
21
+ 201 Created
22
+
23
+ challenge: ${{ challenge }}
24
+ identity: ${{ identity.id }}
25
+ timeout: 60000
26
+ pubKeyCredParams:
27
+ - type: public-key
28
+ alg: -7
29
+ - type: public-key
30
+ alg: -257
31
+ """
32
+
33
+ Scenario: Create a challenge to create a passkey to new identity
34
+ Given the `identity.passkeys` configuration:
35
+ """yaml
36
+ name: Nex
37
+ """
38
+ And transient identity
39
+ When the following request is received:
40
+ """
41
+ POST /identity/passkeys/challenges/ HTTP/1.1
42
+ host: nex.toa.io
43
+ accept: application/yaml
44
+ content-type: application/yaml
45
+
46
+ type: creation
15
47
  """
16
48
  Then the following reply is sent:
17
49
  """
18
50
  201 Created
51
+ authorization: Token ${{ token }}
19
52
 
20
53
  challenge: ${{ challenge }}
54
+ identity: ${{ identity }}
21
55
  timeout: 60000
22
56
  pubKeyCredParams:
23
57
  - type: public-key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/extensions.exposition",
3
- "version": "1.0.0-alpha.130",
3
+ "version": "1.0.0-alpha.132",
4
4
  "description": "Toa Exposition",
5
5
  "author": "temich <tema.gurtovoy@gmail.com>",
6
6
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -63,5 +63,5 @@
63
63
  "@types/negotiator": "0.6.1",
64
64
  "jest-esbuild": "0.3.0"
65
65
  },
66
- "gitHead": "fe6810a087b7c7c88b4e194b99ce421fc21501ba"
66
+ "gitHead": "0159315b61cab2a7ab70e8473e4b88dc666c28d5"
67
67
  }
@@ -1,4 +1,5 @@
1
1
  import assert from 'node:assert'
2
+ import { console } from 'openspan'
2
3
  import * as http from '../../HTTP'
3
4
  import { split } from './split'
4
5
  import { create } from './create'
@@ -35,12 +36,21 @@ export class Incept implements Directive {
35
36
  public async settle (context: Context, response: http.OutgoingMessage): Promise<void> {
36
37
  const id = response.body?.[this.property ?? 'id']
37
38
 
39
+ if (id === undefined) {
40
+ console.debug('Inception skipped: response does not contain expected property', {
41
+ property: this.property,
42
+ response
43
+ })
44
+
45
+ return
46
+ }
47
+
38
48
  assert(typeof id === 'string', `Response body property "${this.property}" expected to be a string`)
39
49
 
40
50
  if (context.request.headers.authorization !== undefined)
41
51
  context.identity = await this.incept(context, id)
42
52
  else
43
- context.identity = { id, scheme: null, refresh: true }
53
+ context.identity = { id, scheme: null, refresh: true, roles: [] }
44
54
  }
45
55
 
46
56
  private async incept (context: Context, id: string): Promise<Identity> {
@@ -67,6 +77,7 @@ export class Incept implements Directive {
67
77
  throw new http.UnprocessableEntity(identity)
68
78
 
69
79
  identity.scheme = scheme
80
+ identity.roles = []
70
81
 
71
82
  return identity
72
83
  }
@@ -28,6 +28,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.Incept = void 0;
30
30
  const node_assert_1 = __importDefault(require("node:assert"));
31
+ const openspan_1 = require("openspan");
31
32
  const http = __importStar(require("../../HTTP"));
32
33
  const split_1 = require("./split");
33
34
  const create_1 = require("./create");
@@ -52,11 +53,18 @@ class Incept {
52
53
  }
53
54
  async settle(context, response) {
54
55
  const id = response.body?.[this.property ?? 'id'];
56
+ if (id === undefined) {
57
+ openspan_1.console.debug('Inception skipped: response does not contain expected property', {
58
+ property: this.property,
59
+ response
60
+ });
61
+ return;
62
+ }
55
63
  (0, node_assert_1.default)(typeof id === 'string', `Response body property "${this.property}" expected to be a string`);
56
64
  if (context.request.headers.authorization !== undefined)
57
65
  context.identity = await this.incept(context, id);
58
66
  else
59
- context.identity = { id, scheme: null, refresh: true };
67
+ context.identity = { id, scheme: null, refresh: true, roles: [] };
60
68
  }
61
69
  async incept(context, id) {
62
70
  const [scheme, credentials] = (0, split_1.split)(context.request.headers.authorization);
@@ -76,6 +84,7 @@ class Incept {
76
84
  if (identity instanceof Error)
77
85
  throw new http.UnprocessableEntity(identity);
78
86
  identity.scheme = scheme;
87
+ identity.roles = [];
79
88
  return identity;
80
89
  }
81
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Incept.js","sourceRoot":"","sources":["../../../source/directives/auth/Incept.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAAgC;AAChC,iDAAkC;AAClC,mCAA+B;AAC/B,qCAAiC;AACjC,uCAAgD;AAIhD,MAAa,MAAM;IACA,QAAQ,CAAe;IACvB,SAAS,CAAW;IACpB,OAAO,GAAY,EAAwB,CAAA;IAE5D,YAAoB,QAAgB,EAAE,SAAoB;QACxD,qBAAM,CAAC,EAAE,CAAC,QAAQ,KAAK,IAAI,IAAI,OAAO,QAAQ,KAAK,QAAQ,EACzD,8CAA8C,CAAC,CAAA;QAEjD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;IAC5B,CAAC;IAEM,SAAS,CAAE,QAAyB;QACzC,OAAO,QAAQ,KAAK,IAAI,CAAA;IAC1B,CAAC;IAEM,KAAK,CAAE,OAAgB;QAC5B,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;YACxB,OAAO,IAAI,CAAA;QAEb,MAAM,IAAI,GAAG,IAAA,eAAM,EAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;QAE1D,OAAO,EAAE,IAAI,EAAE,CAAA;IACjB,CAAC;IAEM,KAAK,CAAC,MAAM,CAAE,OAAgB,EAAE,QAA8B;QACnE,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAA;QAEjD,IAAA,qBAAM,EAAC,OAAO,EAAE,KAAK,QAAQ,EAAE,2BAA2B,IAAI,CAAC,QAAQ,2BAA2B,CAAC,CAAA;QAEnG,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,KAAK,SAAS;YACrD,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;;YAEjD,OAAO,CAAC,QAAQ,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;IAC1D,CAAC;IAEO,KAAK,CAAC,MAAM,CAAE,OAAgB,EAAE,EAAU;QAChD,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAA,aAAK,EAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,aAAc,CAAC,CAAA;QAC3E,MAAM,QAAQ,GAAG,mBAAS,CAAC,MAAM,CAAC,CAAA;QAElC,IAAI,QAAQ,KAAK,SAAS;YACxB,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,wCAAwC,CAAC,CAAA;QAErE,IAAI,CAAC,mBAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC/B,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,2DAA2D,CAAC,CAAA;QAExF,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;QAEvD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAkB,QAAQ,EAAE;YAC5E,KAAK,EAAE;gBACL,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,EAAE;gBACF,WAAW;aACZ;SACF,CAAC,CAAA;QAEF,IAAI,QAAQ,YAAY,KAAK;YAC3B,MAAM,IAAI,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAA;QAE9C,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAA;QAExB,OAAO,QAAQ,CAAA;IACjB,CAAC;CACF;AAhED,wBAgEC"}
1
+ {"version":3,"file":"Incept.js","sourceRoot":"","sources":["../../../source/directives/auth/Incept.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAAgC;AAChC,uCAAkC;AAClC,iDAAkC;AAClC,mCAA+B;AAC/B,qCAAiC;AACjC,uCAAgD;AAIhD,MAAa,MAAM;IACA,QAAQ,CAAe;IACvB,SAAS,CAAW;IACpB,OAAO,GAAY,EAAwB,CAAA;IAE5D,YAAoB,QAAgB,EAAE,SAAoB;QACxD,qBAAM,CAAC,EAAE,CAAC,QAAQ,KAAK,IAAI,IAAI,OAAO,QAAQ,KAAK,QAAQ,EACzD,8CAA8C,CAAC,CAAA;QAEjD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;IAC5B,CAAC;IAEM,SAAS,CAAE,QAAyB;QACzC,OAAO,QAAQ,KAAK,IAAI,CAAA;IAC1B,CAAC;IAEM,KAAK,CAAE,OAAgB;QAC5B,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;YACxB,OAAO,IAAI,CAAA;QAEb,MAAM,IAAI,GAAG,IAAA,eAAM,EAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;QAE1D,OAAO,EAAE,IAAI,EAAE,CAAA;IACjB,CAAC;IAEM,KAAK,CAAC,MAAM,CAAE,OAAgB,EAAE,QAA8B;QACnE,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAA;QAEjD,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;YACrB,kBAAO,CAAC,KAAK,CAAC,gEAAgE,EAAE;gBAC9E,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,QAAQ;aACT,CAAC,CAAA;YAEF,OAAM;QACR,CAAC;QAED,IAAA,qBAAM,EAAC,OAAO,EAAE,KAAK,QAAQ,EAAE,2BAA2B,IAAI,CAAC,QAAQ,2BAA2B,CAAC,CAAA;QAEnG,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,aAAa,KAAK,SAAS;YACrD,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;;YAEjD,OAAO,CAAC,QAAQ,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;IACrE,CAAC;IAEO,KAAK,CAAC,MAAM,CAAE,OAAgB,EAAE,EAAU;QAChD,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAA,aAAK,EAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,aAAc,CAAC,CAAA;QAC3E,MAAM,QAAQ,GAAG,mBAAS,CAAC,MAAM,CAAC,CAAA;QAElC,IAAI,QAAQ,KAAK,SAAS;YACxB,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,wCAAwC,CAAC,CAAA;QAErE,IAAI,CAAC,mBAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC/B,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,2DAA2D,CAAC,CAAA;QAExF,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;QAEvD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAkB,QAAQ,EAAE;YAC5E,KAAK,EAAE;gBACL,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,EAAE;gBACF,WAAW;aACZ;SACF,CAAC,CAAA;QAEF,IAAI,QAAQ,YAAY,KAAK;YAC3B,MAAM,IAAI,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAA;QAE9C,QAAQ,CAAC,MAAM,GAAG,MAAM,CAAA;QACxB,QAAQ,CAAC,KAAK,GAAG,EAAE,CAAA;QAEnB,OAAO,QAAQ,CAAA;IACjB,CAAC;CACF;AA1ED,wBA0EC"}