@owlmeans/server-auth 0.1.18-rc.8 → 0.1.18

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 (218) hide show
  1. package/README.md +216 -38
  2. package/agent-meta/manifest.json +10 -3
  3. package/agent-meta/skills/server-auth/SKILL.md +161 -22
  4. package/agent-meta/skills/supervisor-auth/SKILL.md +128 -0
  5. package/build/actions/service.d.ts +6 -2
  6. package/build/actions/service.d.ts.map +1 -1
  7. package/build/actions/service.js +5 -4
  8. package/build/actions/service.js.map +1 -1
  9. package/build/actions/types.local.d.ts +6 -0
  10. package/build/actions/types.local.d.ts.map +1 -0
  11. package/build/actions/types.local.js +2 -0
  12. package/build/actions/types.local.js.map +1 -0
  13. package/build/entrypoints.d.ts +5 -0
  14. package/build/entrypoints.d.ts.map +1 -0
  15. package/build/entrypoints.js +9 -0
  16. package/build/entrypoints.js.map +1 -0
  17. package/build/index.d.ts +2 -1
  18. package/build/index.d.ts.map +1 -1
  19. package/build/index.js +2 -1
  20. package/build/index.js.map +1 -1
  21. package/build/manager/actions/authenticate.d.ts +8 -0
  22. package/build/manager/actions/authenticate.d.ts.map +1 -0
  23. package/build/manager/actions/authenticate.js +5 -0
  24. package/build/manager/actions/authenticate.js.map +1 -0
  25. package/build/manager/actions/index.d.ts +3 -1
  26. package/build/manager/actions/index.d.ts.map +1 -1
  27. package/build/manager/actions/index.js +3 -1
  28. package/build/manager/actions/index.js.map +1 -1
  29. package/build/manager/actions/init.d.ts +8 -0
  30. package/build/manager/actions/init.d.ts.map +1 -0
  31. package/build/manager/actions/init.js +5 -0
  32. package/build/manager/actions/init.js.map +1 -0
  33. package/build/manager/actions/rely.d.ts +8 -0
  34. package/build/manager/actions/rely.d.ts.map +1 -0
  35. package/build/manager/actions/rely.js +7 -0
  36. package/build/manager/actions/rely.js.map +1 -0
  37. package/build/manager/consts.local.d.ts +2 -0
  38. package/build/manager/consts.local.d.ts.map +1 -0
  39. package/build/manager/consts.local.js +2 -0
  40. package/build/manager/consts.local.js.map +1 -0
  41. package/build/manager/context.d.ts.map +1 -1
  42. package/build/manager/context.js +0 -1
  43. package/build/manager/context.js.map +1 -1
  44. package/build/manager/entrypoints.d.ts +9 -0
  45. package/build/manager/entrypoints.d.ts.map +1 -0
  46. package/build/manager/entrypoints.js +16 -0
  47. package/build/manager/entrypoints.js.map +1 -0
  48. package/build/manager/exports.d.ts +1 -2
  49. package/build/manager/exports.d.ts.map +1 -1
  50. package/build/manager/exports.js +1 -2
  51. package/build/manager/exports.js.map +1 -1
  52. package/build/manager/index.d.ts +1 -1
  53. package/build/manager/index.d.ts.map +1 -1
  54. package/build/manager/index.js +1 -1
  55. package/build/manager/index.js.map +1 -1
  56. package/build/manager/main.d.ts.map +1 -1
  57. package/build/manager/main.js +2 -2
  58. package/build/manager/main.js.map +1 -1
  59. package/build/manager/model.d.ts.map +1 -1
  60. package/build/manager/model.js +71 -32
  61. package/build/manager/model.js.map +1 -1
  62. package/build/manager/plugins/basic-ed25519.d.ts +1 -1
  63. package/build/manager/plugins/basic-ed25519.d.ts.map +1 -1
  64. package/build/manager/plugins/basic-ed25519.js +6 -6
  65. package/build/manager/plugins/basic-ed25519.js.map +1 -1
  66. package/build/manager/plugins/basic-rely.d.ts +1 -1
  67. package/build/manager/plugins/basic-rely.d.ts.map +1 -1
  68. package/build/manager/plugins/basic-rely.js +11 -8
  69. package/build/manager/plugins/basic-rely.js.map +1 -1
  70. package/build/manager/plugins/consts.d.ts +8 -0
  71. package/build/manager/plugins/consts.d.ts.map +1 -0
  72. package/build/manager/plugins/consts.js +9 -0
  73. package/build/manager/plugins/consts.js.map +1 -0
  74. package/build/manager/plugins/export.d.ts +5 -0
  75. package/build/manager/plugins/export.d.ts.map +1 -1
  76. package/build/manager/plugins/export.js +4 -0
  77. package/build/manager/plugins/export.js.map +1 -1
  78. package/build/manager/plugins/index.d.ts +3 -4
  79. package/build/manager/plugins/index.d.ts.map +1 -1
  80. package/build/manager/plugins/index.js +6 -6
  81. package/build/manager/plugins/index.js.map +1 -1
  82. package/build/manager/plugins/re-captcha/consts.d.ts +7 -0
  83. package/build/manager/plugins/re-captcha/consts.d.ts.map +1 -0
  84. package/build/manager/plugins/re-captcha/consts.js +7 -0
  85. package/build/manager/plugins/re-captcha/consts.js.map +1 -0
  86. package/build/manager/plugins/re-captcha/consts.local.d.ts +7 -0
  87. package/build/manager/plugins/re-captcha/consts.local.d.ts.map +1 -0
  88. package/build/manager/plugins/re-captcha/consts.local.js +9 -0
  89. package/build/manager/plugins/re-captcha/consts.local.js.map +1 -0
  90. package/build/manager/plugins/re-captcha/policy.d.ts +3 -0
  91. package/build/manager/plugins/re-captcha/policy.d.ts.map +1 -0
  92. package/build/manager/plugins/re-captcha/policy.js +44 -0
  93. package/build/manager/plugins/re-captcha/policy.js.map +1 -0
  94. package/build/manager/plugins/re-captcha/types.d.ts +79 -0
  95. package/build/manager/plugins/re-captcha/types.d.ts.map +1 -0
  96. package/build/manager/plugins/re-captcha/types.js +2 -0
  97. package/build/manager/plugins/re-captcha/types.js.map +1 -0
  98. package/build/manager/plugins/re-captcha/verifier.d.ts +12 -0
  99. package/build/manager/plugins/re-captcha/verifier.d.ts.map +1 -0
  100. package/build/manager/plugins/re-captcha/verifier.js +62 -0
  101. package/build/manager/plugins/re-captcha/verifier.js.map +1 -0
  102. package/build/manager/plugins/re-captcha.d.ts +10 -1
  103. package/build/manager/plugins/re-captcha.d.ts.map +1 -1
  104. package/build/manager/plugins/re-captcha.js +50 -37
  105. package/build/manager/plugins/re-captcha.js.map +1 -1
  106. package/build/manager/plugins/supervisor.d.ts +1 -2
  107. package/build/manager/plugins/supervisor.d.ts.map +1 -1
  108. package/build/manager/plugins/supervisor.js +6 -6
  109. package/build/manager/plugins/supervisor.js.map +1 -1
  110. package/build/manager/plugins/types.d.ts +22 -14
  111. package/build/manager/plugins/types.d.ts.map +1 -1
  112. package/build/manager/plugins/utils.d.ts +5 -4
  113. package/build/manager/plugins/utils.d.ts.map +1 -1
  114. package/build/manager/plugins/utils.js +20 -15
  115. package/build/manager/plugins/utils.js.map +1 -1
  116. package/build/manager/rely/flow.d.ts.map +1 -1
  117. package/build/manager/rely/flow.js +13 -11
  118. package/build/manager/rely/flow.js.map +1 -1
  119. package/build/manager/rely.d.ts.map +1 -1
  120. package/build/manager/rely.js.map +1 -1
  121. package/build/manager/supervisor.d.ts +4 -45
  122. package/build/manager/supervisor.d.ts.map +1 -1
  123. package/build/manager/supervisor.js +5 -5
  124. package/build/manager/supervisor.js.map +1 -1
  125. package/build/manager/types.d.ts +41 -1
  126. package/build/manager/types.d.ts.map +1 -1
  127. package/build/manager/types.local.d.ts +4 -0
  128. package/build/manager/types.local.d.ts.map +1 -0
  129. package/build/manager/types.local.js +2 -0
  130. package/build/manager/types.local.js.map +1 -0
  131. package/build/manager/utils/trusted.d.ts.map +1 -1
  132. package/build/manager/utils/trusted.js.map +1 -1
  133. package/build/model/rely/consumer.d.ts +3 -1
  134. package/build/model/rely/consumer.d.ts.map +1 -1
  135. package/build/model/rely/consumer.js.map +1 -1
  136. package/build/model/rely/peer.d.ts.map +1 -1
  137. package/build/model/rely/peer.js +7 -7
  138. package/build/model/rely/peer.js.map +1 -1
  139. package/build/model/rely/provider.d.ts +3 -1
  140. package/build/model/rely/provider.d.ts.map +1 -1
  141. package/build/model/rely/provider.js.map +1 -1
  142. package/build/re-captcha/guard.d.ts +12 -0
  143. package/build/re-captcha/guard.d.ts.map +1 -0
  144. package/build/re-captcha/guard.js +43 -0
  145. package/build/re-captcha/guard.js.map +1 -0
  146. package/build/re-captcha/index.d.ts +4 -0
  147. package/build/re-captcha/index.d.ts.map +1 -0
  148. package/build/re-captcha/index.js +3 -0
  149. package/build/re-captcha/index.js.map +1 -0
  150. package/build/re-captcha/token.d.ts +6 -0
  151. package/build/re-captcha/token.d.ts.map +1 -0
  152. package/build/re-captcha/token.js +74 -0
  153. package/build/re-captcha/token.js.map +1 -0
  154. package/build/re-captcha/types.d.ts +44 -0
  155. package/build/re-captcha/types.d.ts.map +1 -0
  156. package/build/re-captcha/types.js +2 -0
  157. package/build/re-captcha/types.js.map +1 -0
  158. package/build/service.d.ts +2 -3
  159. package/build/service.d.ts.map +1 -1
  160. package/build/service.js +136 -13
  161. package/build/service.js.map +1 -1
  162. package/build/types.local.d.ts +4 -0
  163. package/build/types.local.d.ts.map +1 -0
  164. package/build/types.local.js +2 -0
  165. package/build/types.local.js.map +1 -0
  166. package/package.json +33 -30
  167. package/src/actions/service.ts +10 -12
  168. package/src/actions/types.local.ts +6 -0
  169. package/src/entrypoints.ts +12 -0
  170. package/src/index.ts +2 -2
  171. package/src/manager/README.md +14 -18
  172. package/src/manager/actions/authenticate.ts +12 -0
  173. package/src/manager/actions/index.ts +3 -1
  174. package/src/manager/actions/init.ts +12 -0
  175. package/src/manager/actions/rely.ts +12 -0
  176. package/src/manager/consts.local.ts +1 -0
  177. package/src/manager/context.ts +0 -2
  178. package/src/manager/entrypoints.ts +17 -0
  179. package/src/manager/exports.ts +1 -2
  180. package/src/manager/index.ts +1 -1
  181. package/src/manager/main.ts +2 -2
  182. package/src/manager/model.ts +82 -41
  183. package/src/manager/plugins/basic-ed25519.ts +8 -8
  184. package/src/manager/plugins/basic-rely.ts +14 -9
  185. package/src/manager/plugins/consts.ts +7 -0
  186. package/src/manager/plugins/export.ts +5 -0
  187. package/src/manager/plugins/index.ts +10 -12
  188. package/src/manager/plugins/re-captcha/consts.local.ts +8 -0
  189. package/src/manager/plugins/re-captcha/consts.ts +8 -0
  190. package/src/manager/plugins/re-captcha/policy.ts +56 -0
  191. package/src/manager/plugins/re-captcha/types.ts +85 -0
  192. package/src/manager/plugins/re-captcha/verifier.ts +74 -0
  193. package/src/manager/plugins/re-captcha.ts +58 -43
  194. package/src/manager/plugins/supervisor.ts +9 -11
  195. package/src/manager/plugins/types.ts +23 -14
  196. package/src/manager/plugins/utils.ts +24 -14
  197. package/src/manager/rely/flow.ts +14 -11
  198. package/src/manager/rely.ts +2 -5
  199. package/src/manager/supervisor.ts +8 -54
  200. package/src/manager/types.local.ts +5 -0
  201. package/src/manager/types.ts +46 -1
  202. package/src/model/rely/consumer.ts +3 -1
  203. package/src/model/rely/peer.ts +8 -8
  204. package/src/model/rely/provider.ts +3 -1
  205. package/src/re-captcha/guard.ts +52 -0
  206. package/src/re-captcha/index.ts +3 -0
  207. package/src/re-captcha/token.ts +89 -0
  208. package/src/re-captcha/types.ts +45 -0
  209. package/src/service.ts +133 -19
  210. package/src/types.local.ts +5 -0
  211. package/tests/context.ts +3 -2
  212. package/tests/re-captcha.spec.ts +298 -0
  213. package/tests/replay-policy.spec.ts +36 -0
  214. package/tests/supervisor.spec.ts +6 -4
  215. package/build/.gitkeep +0 -0
  216. package/src/manager/actions/manager.ts +0 -23
  217. package/src/manager/modules.ts +0 -29
  218. package/src/modules.ts +0 -17
@@ -1,59 +1,74 @@
1
- import { AuthenFailed, AuthenticationType, AuthPluginError, MOD_RECAPTCHA, AUTH_SCOPE, AuthRole } from '@owlmeans/auth'
2
- import type { AppContext, AppConfig } from '../types.js'
3
- import type { AuthPlugin, RecaptchaRequest, RecpatchaResponse } from './types.js'
4
- import { PLUGINS } from '@owlmeans/config'
5
- import type { PluginConfig } from '@owlmeans/config'
6
- import { PluginMissconfigured } from '@owlmeans/config'
7
- import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
8
- import { EntrypointOutcome } from '@owlmeans/entrypoint'
1
+ import { AUTH_SCOPE, AuthenticationType, AuthRole, GUEST_ID, MOD_RECAPTCHA } from '@owlmeans/auth'
2
+ import type { AuthCredentials } from '@owlmeans/auth'
3
+ import { PLUGINS, PluginMissconfigured } from '@owlmeans/config'
9
4
  import { base64 } from '@scure/base'
10
- import { randomBytes } from '@noble/hashes/utils'
11
- import { assertType } from './utils.js'
5
+ import { randomBytes } from '@noble/hashes/utils.js'
6
+ import type { AppContext, AppConfig } from '../types.js'
7
+ import type { AuthPlugin } from './types.js'
8
+ import { authPluginHelper } from './utils.js'
9
+ import { RECAPTCHA_VERIFIER } from './re-captcha/consts.js'
10
+ import { RECAPTCHA_CREDENTIAL_FIELDS } from './re-captcha/consts.local.js'
11
+ import { makeReCaptchaPolicyModel } from './re-captcha/policy.js'
12
+ import { createReCaptchaVerifierService } from './re-captcha/verifier.js'
13
+ import type { ReCaptchaPluginConfig, ReCaptchaPolicyModel, ReCaptchaVerifierService } from './re-captcha/types.js'
14
+
15
+ /**
16
+ * The reCAPTCHA sign-in: proves that a person solved a challenge, and nothing about who they are.
17
+ *
18
+ * The token the manager signs for it is a GUEST credential — `GUEST_ID`, role Guest, `[AUTH_SCOPE]`,
19
+ * type ReCaptcha — whatever identity the caller posted: a profile, an organization, permissions,
20
+ * groups, an expiry and a `source` are dropped before it is signed. Its single-use key is the
21
+ * server-issued challenge it carries. A route accepts it only through `makeReCaptchaGuard`; the
22
+ * bearer exchange refuses it.
23
+ */
24
+ export const makeReCaptchaPlugin = <C extends AppConfig, T extends AppContext<C>>(context: T): AuthPlugin => {
25
+ const verifier = (): ReCaptchaVerifierService => context.hasService(RECAPTCHA_VERIFIER)
26
+ ? context.service<ReCaptchaVerifierService>(RECAPTCHA_VERIFIER)
27
+ : createReCaptchaVerifierService()
28
+
29
+ /** The secret and the policy of the `MOD_RECAPTCHA` record; no secret = no reCAPTCHA sign-in. */
30
+ const configured = async (): Promise<[string, ReCaptchaPolicyModel]> => {
31
+ const record = await context.getConfigResource<ReCaptchaPluginConfig>(PLUGINS).load(MOD_RECAPTCHA)
32
+ if (record == null || typeof record.value !== 'string' || record.value === '') {
33
+ throw new PluginMissconfigured('value')
34
+ }
35
+
36
+ return [record.value, makeReCaptchaPolicyModel(record)]
37
+ }
38
+
39
+ const resetIdentity = (credential: AuthCredentials): void => {
40
+ const fields = credential as unknown as Record<string, unknown>
41
+ for (const field of Object.keys(fields)) {
42
+ if (!RECAPTCHA_CREDENTIAL_FIELDS.includes(field)) {
43
+ delete fields[field]
44
+ }
45
+ }
46
+ credential.type = AuthenticationType.ReCaptcha
47
+ credential.role = AuthRole.Guest
48
+ credential.userId = GUEST_ID
49
+ credential.scopes = [AUTH_SCOPE]
50
+ }
12
51
 
13
- export const reCaptcha = <C extends AppConfig, T extends AppContext<C>>(context: T): AuthPlugin => {
14
52
  const plugin: AuthPlugin = {
15
53
  type: AuthenticationType.ReCaptcha,
16
54
 
17
55
  init: async request => {
18
- assertType(request.type, plugin)
56
+ authPluginHelper.assertType(request.type, plugin)
19
57
 
20
- const challenge = base64.encode(randomBytes(32))
21
-
22
- return { challenge }
58
+ return { challenge: base64.encode(randomBytes(32)) }
23
59
  },
24
60
 
25
61
  authenticate: async credential => {
26
- const cfg = context.getConfigResource(PLUGINS)
27
- const config = await cfg.get<PluginConfig>(MOD_RECAPTCHA)
28
- if (config.value == null) {
29
- throw new PluginMissconfigured('value')
30
- }
31
- const validateRecaptcha = context.entrypoint<ClientEntrypoint<RecpatchaResponse>>(MOD_RECAPTCHA)
32
- const [result, status] = await validateRecaptcha.call<RecpatchaResponse, RecaptchaRequest>({
33
- body: {
34
- secret: config.value,
35
- response: credential.credential
36
- },
37
- headers: { 'content-type': 'application/x-www-form-urlencoded' }
38
- })
39
- if (status !== EntrypointOutcome.Ok) {
40
- throw new AuthPluginError('recaptcha:api')
41
- }
42
- if (!result.success) {
43
- throw new AuthenFailed('recaptcha:' + (result['error-codes']?.join(',') ?? 'unknown'))
44
- }
62
+ const [secret, policy] = await configured()
63
+ // Read before the policy is judged: a misconfigured record fails before Google is asked.
64
+ policy.minScore()
45
65
 
46
- credential.scopes = [AUTH_SCOPE]
47
- credential.role = AuthRole.Guest
48
- credential.type = AuthenticationType.ReCaptcha
66
+ const answer = await verifier().verify({ secret, response: credential.credential })
67
+ policy.assert(answer)
49
68
 
50
- const [previous, challenge] = credential.challenge.split(':') as [string, string | undefined]
51
- if (challenge != null) {
52
- credential.challenge = previous
53
-
54
- return { token: previous }
55
- }
69
+ resetIdentity(credential)
56
70
 
71
+ // '' keeps the manager's own server-issued challenge on the token: that is its spend key.
57
72
  return { token: '' }
58
73
  }
59
74
  }
@@ -1,13 +1,11 @@
1
- import { AuthenticationType, AuthenFailed, AuthRole, ALL_SCOPES, buildSupervisorPayload } from '@owlmeans/auth'
2
- import type { SupervisorCredentialPayload } from '@owlmeans/auth'
1
+ import { AuthenticationType, AuthenFailed, AuthRole, ALL_SCOPES, buildSupervisorPayload, type SupervisorCredentialPayload } from '@owlmeans/auth'
3
2
  import type { AuthPlugin } from './types.js'
4
3
  import { base64 } from '@scure/base'
5
- import { randomBytes } from '@noble/hashes/utils'
6
- import { fromPubKey } from '@owlmeans/basic-keys'
4
+ import { randomBytes } from '@noble/hashes/utils.js'
5
+ import { keyHelper } from '@owlmeans/basic-keys'
7
6
  import { TRUSTED } from '@owlmeans/config'
8
7
  import type { TrustedRecord } from '@owlmeans/auth-common'
9
- import type { AppConfig, AppContext } from '../types.js'
10
- import type { SupervisorPluginOptions } from '../supervisor.js'
8
+ import type { AppConfig, AppContext, SupervisorPluginOptions } from '../types.js'
11
9
 
12
10
  /**
13
11
  * PK-based supervisor authentication plugin. A privileged operator (or an e2e
@@ -48,12 +46,12 @@ export const makeSupervisorPlugin = (
48
46
  let matched: string | null = null
49
47
  for (const alias of opts.supervisors) {
50
48
  try {
51
- const record = await context.getConfigResource(TRUSTED)
52
- .load<TrustedRecord>(alias, 'name')
49
+ const record = await context.getConfigResource<TrustedRecord>(TRUSTED)
50
+ .load({ name: alias })
53
51
  if (record?.credential == null) {
54
52
  continue
55
53
  }
56
- if (await fromPubKey(record.credential).verify(payload, parsed.signature)) {
54
+ if (await keyHelper.fromPubKey(record.credential).verify(payload, parsed.signature)) {
57
55
  matched = alias
58
56
  break
59
57
  }
@@ -71,8 +69,8 @@ export const makeSupervisorPlugin = (
71
69
 
72
70
  credential.userId = resolution.userId
73
71
  credential.profileId = resolution.profileId ?? resolution.userId
74
- if (resolution.entityId != null) {
75
- credential.entityId = resolution.entityId
72
+ if (resolution.entitySlug != null) {
73
+ credential.entitySlug = resolution.entitySlug
76
74
  }
77
75
  credential.scopes = resolution.scopes ?? credential.scopes ?? [ALL_SCOPES]
78
76
  credential.role = resolution.role ?? AuthRole.User
@@ -1,27 +1,36 @@
1
1
  import type { AllowanceResponse } from '@owlmeans/auth'
2
- import type { AuthModel } from '../types.js'
3
- import type { AbstractRequest } from '@owlmeans/entrypoint'
2
+ import type { AppConfig, AppContext, AuthModel } from '../types.js'
4
3
  import type { RedisResource } from '@owlmeans/redis-resource'
5
4
  import type { ResourceRecord } from '@owlmeans/resource'
5
+ import type { AuthChallengeReplayPolicy } from './consts.js'
6
6
 
7
7
  export interface AuthPlugin extends Omit<AuthModel, "rely"> {
8
8
  type: string
9
+ /**
10
+ * Who atomically consumes a verified challenge. The manager remains the default; a plugin may
11
+ * opt in only when its own credential protocol needs a bounded retry budget before consumption.
12
+ */
13
+ challengeReplayPolicy?: AuthChallengeReplayPolicy
9
14
  }
10
15
 
11
- export interface RecpatchaResponse {
12
- success: boolean
13
- challenge_ts: number
14
- hostname: string
15
- 'error-codes'?: string[]
16
+ export interface RelyRecord extends ResourceRecord, AllowanceResponse {
16
17
  }
17
18
 
18
- export interface RecaptchaRequest extends AbstractRequest<{
19
- secret: string
20
- response: string
21
- remoteip?: string
22
- }> { }
19
+ export interface AuthRedisResource extends RedisResource<RelyRecord> {}
23
20
 
24
- export interface RelyRecord extends ResourceRecord, AllowanceResponse {
21
+ /** Builds the plugin of one authentication type over the context it serves. */
22
+ export interface AuthPluginFactory {
23
+ <C extends AppConfig, T extends AppContext<C>>(context: T): AuthPlugin
25
24
  }
26
25
 
27
- export interface AuthRedisResource extends RedisResource<RelyRecord> {}
26
+ /** Resolution of the registered plugins, and the check every plugin runs on what it is handed. */
27
+ export interface AuthPluginHelper {
28
+ /** @throws {TypeMissmatchError} when a request names another plugin's type */
29
+ assertType: (type: string, plugin: AuthPlugin) => void
30
+ /**
31
+ * The registered plugin of a type, built over the context.
32
+ *
33
+ * @throws {AuthUnknown}
34
+ */
35
+ getPlugin: (type: string, context: AppContext<AppConfig>) => Promise<AuthPlugin>
36
+ }
@@ -1,21 +1,31 @@
1
1
  import { AuthUnknown, TypeMissmatchError } from '@owlmeans/auth'
2
- import type { AuthPlugin } from './types.js'
2
+ import type { AuthPlugin, AuthPluginHelper } from './types.js'
3
3
  import type { AppContext, AppConfig } from '../types.js'
4
4
 
5
- export const assertType = (type: string, plugin: AuthPlugin) => {
6
- if (type !== plugin.type) {
7
- throw new TypeMissmatchError(plugin.type)
5
+ export const createAuthPluginHelper = (): AuthPluginHelper => {
6
+ const assertType = (type: string, plugin: AuthPlugin): void => {
7
+ if (type !== plugin.type) {
8
+ throw new TypeMissmatchError(plugin.type)
9
+ }
8
10
  }
9
- }
10
11
 
11
- /**
12
- * @throws {AuthUnknown}
13
- */
14
- export const getPlugin = async (type: string, context: AppContext<AppConfig>): Promise<AuthPlugin> => {
15
- const { plugins } = await import('./index.js')
16
- const plugin = plugins[type]
17
- if (plugin == null) {
18
- throw new AuthUnknown(type)
12
+ const getPlugin = async (type: string, context: AppContext<AppConfig>): Promise<AuthPlugin> => {
13
+ const { plugins } = await import('./index.js')
14
+ const plugin = plugins[type]
15
+ if (plugin == null) {
16
+ throw new AuthUnknown(type)
17
+ }
18
+ return plugins[type](context)
19
19
  }
20
- return plugins[type](context)
20
+
21
+ return { assertType, getPlugin }
21
22
  }
23
+
24
+ export const authPluginHelper = createAuthPluginHelper()
25
+
26
+ /** @deprecated compat:factory-refactor — use `authPluginHelper.assertType(…)` */
27
+ export const assertType = (type: string, plugin: AuthPlugin): void => authPluginHelper.assertType(type, plugin)
28
+
29
+ /** @deprecated compat:factory-refactor — use `authPluginHelper.getPlugin(…)` */
30
+ export const getPlugin = async (type: string, context: AppContext<AppConfig>): Promise<AuthPlugin> =>
31
+ await authPluginHelper.getPlugin(type, context)
@@ -1,7 +1,6 @@
1
1
  import type { AllowanceRequest, Auth, AuthCredentials, RelyToken } from '@owlmeans/auth'
2
2
  import { AUTH_SCOPE, AuthenticationStage, AuthenticationType, AuthPluginError, AuthRole, RELY_3RD } from '@owlmeans/auth'
3
3
  import type { AuthenticateMethod, Connection, EventMessage, Message } from '@owlmeans/socket'
4
- import { isEventMessage, isMessage } from '@owlmeans/socket'
5
4
  import type { AppContext, RelyAllowanceRequest, RelyLinker, RelyCarrier } from '../types.js'
6
5
  import { makeAuthModel } from '../model.js'
7
6
  import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
@@ -9,6 +8,8 @@ import { trusted } from '../utils/trusted.js'
9
8
  import { RELY_TUNNEL } from '../consts.js'
10
9
  import type { RedisResource } from '@owlmeans/redis-resource'
11
10
  import { RELY_ACTION_TIMEOUT } from '@owlmeans/auth-common'
11
+ import { logger } from '@owlmeans/log'
12
+ import { socketMessageHelper } from '@owlmeans/socket'
12
13
 
13
14
  // 1. There is a difference between privileged (provider)
14
15
  // and non-privileged (consumer) request
@@ -20,6 +21,8 @@ import { RELY_ACTION_TIMEOUT } from '@owlmeans/auth-common'
20
21
  // process can be started from scratch following same stages
21
22
  // and using the same plugins but via socket
22
23
 
24
+ const log = logger('server-auth:rely')
25
+
23
26
  export const createRelyFlow = (context: AppContext, conn: Connection, auth?: Auth | null): AuthenticateMethod => {
24
27
  const auhtenticate = conn.authenticate
25
28
 
@@ -29,29 +32,29 @@ export const createRelyFlow = (context: AppContext, conn: Connection, auth?: Aut
29
32
  const linker: RelyLinker = async (rely, source, notify) => {
30
33
  const tunnel = context.resource<RedisResource<Message<any>>>(RELY_TUNNEL)
31
34
  const closeReceiver = await tunnel.subscribe(async message => {
32
- if (isMessage(message, true)) {
35
+ if (socketMessageHelper.isMessage(message, true)) {
33
36
  await conn.send(message)
34
- } else if (isEventMessage(message, true)) {
37
+ } else if (socketMessageHelper.isEventMessage(message, true)) {
35
38
  if ((message as EventMessage<unknown>).event === 'close') {
36
39
  await conn.close()
37
40
  closeSender()
38
41
  await closeReceiver()
39
42
  }
40
43
  }
41
- }, source.nonce)
44
+ }, { channel: source.nonce })
42
45
  // We allow to just forward call messages back and forth
43
- conn._receiveCall = async msg => { console.info(source.nonce, 'Forward call', msg.method, msg.id) }
44
- conn._receiveResult = async msg => { console.info(source.nonce, 'Forward result', msg.id) }
45
- conn._receiveError = async msg => { console.info(source.nonce, 'Forward error', msg.id) }
46
+ conn._receiveCall = async msg => { log.debug('Forward call', { nonce: source.nonce, method: msg.method, id: msg.id }) }
47
+ conn._receiveResult = async msg => { log.debug('Forward result', { nonce: source.nonce, id: msg.id }) }
48
+ conn._receiveError = async msg => { log.debug('Forward error', { nonce: source.nonce, id: msg.id }) }
46
49
  conn.defaultCallTimeout = RELY_ACTION_TIMEOUT * 1000
47
50
  const closeSender = conn.listen(async message => {
48
- if (isMessage(message, true)) {
51
+ if (socketMessageHelper.isMessage(message, true)) {
49
52
  const forward = { ...message }
50
53
  if (forward.rawData != null) {
51
54
  delete forward.rawData
52
55
  }
53
56
  await tunnel.publish(forward, rely.nonce)
54
- } else if (isEventMessage(message, true)) {
57
+ } else if (socketMessageHelper.isEventMessage(message, true)) {
55
58
  if (message.event === 'close') {
56
59
  const forward = { ...message as Message<unknown> }
57
60
  if (forward.rawData != null) {
@@ -94,7 +97,7 @@ export const createRelyFlow = (context: AppContext, conn: Connection, auth?: Aut
94
97
  try {
95
98
  return [stage, await model.init(_payload as AllowanceRequest) as any]
96
99
  } catch (e) {
97
- console.error(e)
100
+ log.warn('Rely init failed; connection closed', e)
98
101
  await conn.close()
99
102
  }
100
103
  break
@@ -118,7 +121,7 @@ export const createRelyFlow = (context: AppContext, conn: Connection, auth?: Aut
118
121
 
119
122
  return [AuthenticationStage.Authenticated, internalAuth as any]
120
123
  } catch (e) {
121
- console.error(e)
124
+ log.warn('Rely authentication failed; connection closed', e)
122
125
  await conn.close()
123
126
  }
124
127
  break
@@ -1,18 +1,15 @@
1
1
  import { assertContext, createService } from '@owlmeans/context'
2
2
  import { DEFAULT_RELY } from './consts.js'
3
3
  import type { AppContext, RelyService } from './types.js'
4
- import { AUTH_QUERY, AuthenFailed, AuthenticationType } from '@owlmeans/auth'
5
- import type { Auth, AuthCredentials, AuthToken } from '@owlmeans/auth'
4
+ import { AUTH_QUERY, AuthenFailed, AuthenticationType, type Auth, type AuthCredentials, type AuthToken } from '@owlmeans/auth'
6
5
  import type { AbstractRequest, AbstractResponse } from '@owlmeans/entrypoint'
7
6
  import { EnvelopeKind, makeEnvelopeModel } from '@owlmeans/basic-envelope'
8
- import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
9
7
  import { AUTH_CACHE, AUTHEN_TIMEFRAME } from '../consts.js'
10
8
  import type { AuthSpent } from '../types.js'
11
9
  import type { Resource } from '@owlmeans/resource'
12
10
  import { trusted } from './utils/trusted.js'
11
+ import type { Config, Context } from './types.local.js'
13
12
 
14
- type Config = ServerConfig
15
- type Context = ServerContext<Config>
16
13
 
17
14
  export const createRelyService = (alias: string = DEFAULT_RELY): RelyService => {
18
15
  const _keyPair = async (context: Context) => {
@@ -1,71 +1,25 @@
1
1
  import { AuthenticationType } from '@owlmeans/auth'
2
- import type { AuthPayload } from '@owlmeans/auth'
3
2
  import { DEFAULT_GUARD } from '@owlmeans/auth-common'
4
- import type { AppConfig, AppContext } from './types.js'
3
+ import type { AppConfig, AppContext, SupervisorAuthOptions, SupervisorPluginOptions } from './types.js'
4
+ import type { AuthPluginFactory } from './plugins/types.js'
5
5
  import { registerPlugin } from './plugins/index.js'
6
6
  import { makeSupervisorPlugin } from './plugins/supervisor.js'
7
-
8
- /**
9
- * What a `SupervisorUserResolver` returns: the identity the supervisor-minted
10
- * token will represent. Only `userId` is required - the rest default sensibly.
11
- */
12
- export interface SupervisorUserResolution extends Partial<Pick<AuthPayload,
13
- 'profileId' | 'entityId' | 'role' | 'scopes'>> {
14
- userId: string
15
- }
16
-
17
- /**
18
- * Find-or-create the target identity for a supervisor login. `register` reflects
19
- * `allowRegistration`; when false the resolver should only look existing users up.
20
- * Wire this to the project's identity store (e.g. `@owlmeans/server-auth-identity`).
21
- */
22
- export interface SupervisorUserResolver {
23
- <C extends AppConfig, T extends AppContext<C>>(
24
- userId: string, context: T, opts: { register: boolean }
25
- ): Promise<SupervisorUserResolution>
26
- }
27
-
28
- /** Resolved options handed to the plugin factory. */
29
- export interface SupervisorPluginOptions {
30
- supervisors: string[]
31
- allowRegistration: boolean
32
- resolveUser?: SupervisorUserResolver
33
- }
34
-
35
- export interface SupervisorAuthOptions {
36
- /** Trusted-record aliases authorized to act as supervisor. Default: master + superuser. */
37
- supervisors?: string[]
38
- /** Find-or-create the target user by id/email. Default: trust the id as-is. */
39
- resolveUser?: SupervisorUserResolver
40
- /** Allow minting a token for an unknown user (registration). Default: true. */
41
- allowRegistration?: boolean
42
- /** Force enable/disable. Default: development only (cfg.debug.all || cfg.debug.supervisor). */
43
- enabled?: boolean
44
- /**
45
- * Also accept internal owlmeans `Ed25519BasicToken`s even when another guard
46
- * (e.g. OIDC) is the primary guard on protected modules. Default: true.
47
- */
48
- acceptInternalTokens?: boolean
49
- /** The internal-token guard to add as a coguard. Default: DEFAULT_GUARD ('auth'). */
50
- guard?: string
51
- }
52
-
53
- const DEFAULT_SUPERVISORS = ['master', 'superuser']
7
+ import { DEFAULT_SUPERVISORS } from './consts.local.js'
54
8
 
55
9
  const isDevelopment = (context: { cfg: { debug?: { all?: boolean, supervisor?: boolean } } }): boolean =>
56
10
  context.cfg.debug?.all === true || context.cfg.debug?.supervisor === true
57
11
 
58
12
  /**
59
13
  * Ensure the internal `Ed25519BasicToken` guard is accepted as a coguard on every
60
- * already-guarded backend module, so internal owlmeans tokens keep working even
14
+ * already-guarded backend entrypoint, so internal owlmeans tokens keep working even
61
15
  * when another guard (e.g. OIDC) is the primary guard. The primary guard stays
62
16
  * first; the internal guard is appended as a fallback (its `match` only fires for
63
17
  * an `Ed25519BasicToken` authorization header).
64
18
  */
65
19
  export const setupInternalTokenCoguard = (
66
- modules: Array<{ guards?: string[] }>, guard: string = DEFAULT_GUARD
20
+ entrypoints: Array<{ guards?: string[] }>, guard: string = DEFAULT_GUARD
67
21
  ): void => {
68
- modules.forEach(module => {
22
+ entrypoints.forEach(module => {
69
23
  if (module.guards != null && module.guards.length > 0 && !module.guards.includes(guard)) {
70
24
  module.guards.push(guard)
71
25
  }
@@ -78,7 +32,7 @@ export const setupInternalTokenCoguard = (
78
32
  *
79
33
  * It registers the supervisor auth plugin (verifies a front-end signature against
80
34
  * the allowlisted trusted keys, then resolves/registers the target user) and -
81
- * unless disabled - makes protected modules also accept internal owlmeans tokens
35
+ * unless disabled - makes protected entrypoints also accept internal owlmeans tokens
82
36
  * (requirement: understand internal tokens even when OIDC is the primary guard).
83
37
  */
84
38
  export const appendSupervisorAuth = <C extends AppConfig, T extends AppContext<C>>(
@@ -97,7 +51,7 @@ export const appendSupervisorAuth = <C extends AppConfig, T extends AppContext<C
97
51
 
98
52
  registerPlugin(
99
53
  AuthenticationType.Supervisor,
100
- (ctx => makeSupervisorPlugin(ctx as unknown as AppContext<AppConfig>, resolved)) as Parameters<typeof registerPlugin>[1]
54
+ (ctx => makeSupervisorPlugin(ctx as unknown as AppContext<AppConfig>, resolved)) as AuthPluginFactory
101
55
  )
102
56
 
103
57
  if (opts?.acceptInternalTokens !== false) {
@@ -0,0 +1,5 @@
1
+ import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
2
+
3
+ export type Config = ServerConfig
4
+
5
+ export type Context = ServerContext<Config>
@@ -1,4 +1,4 @@
1
- import type { AllowanceRequest, AllowanceResponse, Auth, AuthCredentials, AuthToken, RelyToken } from '@owlmeans/auth'
1
+ import type { AllowanceRequest, AllowanceResponse, Auth, AuthCredentials, AuthToken, RelyToken, AuthPayload } from '@owlmeans/auth'
2
2
  import type { KlusterConfig } from '@owlmeans/kluster'
3
3
  import type { GuardService } from '@owlmeans/entrypoint'
4
4
  import type { ApiServerAppend } from '@owlmeans/server-api'
@@ -40,3 +40,48 @@ export interface RelyCarrier {
40
40
  source: RelyToken,
41
41
  rely: RelyToken
42
42
  }
43
+
44
+ /**
45
+ * What a `SupervisorUserResolver` returns: the identity the supervisor-minted
46
+ * token will represent. Only `userId` is required - the rest default sensibly.
47
+ */
48
+ export interface SupervisorUserResolution extends Partial<Pick<AuthPayload,
49
+ 'profileId' | 'entitySlug' | 'role' | 'scopes'>> {
50
+ userId: string
51
+ }
52
+
53
+ /**
54
+ * Find-or-create the target identity for a supervisor login. `register` reflects
55
+ * `allowRegistration`; when false the resolver should only look existing users up.
56
+ * Wire this to the project's identity store (e.g. `@owlmeans/server-auth-identity`).
57
+ */
58
+ export interface SupervisorUserResolver {
59
+ <C extends AppConfig, T extends AppContext<C>>(
60
+ userId: string, context: T, opts: { register: boolean }
61
+ ): Promise<SupervisorUserResolution>
62
+ }
63
+
64
+ /** Resolved options handed to the plugin factory. */
65
+ export interface SupervisorPluginOptions {
66
+ supervisors: string[]
67
+ allowRegistration: boolean
68
+ resolveUser?: SupervisorUserResolver
69
+ }
70
+
71
+ export interface SupervisorAuthOptions {
72
+ /** Trusted-record aliases authorized to act as supervisor. Default: master + superuser. */
73
+ supervisors?: string[]
74
+ /** Find-or-create the target user by id/email. Default: trust the id as-is. */
75
+ resolveUser?: SupervisorUserResolver
76
+ /** Allow minting a token for an unknown user (registration). Default: true. */
77
+ allowRegistration?: boolean
78
+ /** Force enable/disable. Default: development only (cfg.debug.all || cfg.debug.supervisor). */
79
+ enabled?: boolean
80
+ /**
81
+ * Also accept internal owlmeans `Ed25519BasicToken`s even when another guard
82
+ * (e.g. OIDC) is the primary guard on protected entrypoints. Default: true.
83
+ */
84
+ acceptInternalTokens?: boolean
85
+ /** The internal-token guard to add as a coguard. Default: DEFAULT_GUARD ('auth'). */
86
+ guard?: string
87
+ }
@@ -1,7 +1,9 @@
1
1
  import { makeRelyModel } from './peer.js'
2
+ import type { RelyToken } from '@owlmeans/auth'
3
+ import type { EnvelopeModel } from '@owlmeans/basic-envelope'
2
4
  import type { RelyOptions } from './types.js'
3
5
 
4
- export const makeConsumerRely = (opts?: RelyOptions) => {
6
+ export const makeConsumerRely = (opts?: RelyOptions): EnvelopeModel<RelyToken> => {
5
7
  const model = makeRelyModel(opts)
6
8
  const msg = model.message()
7
9
  delete msg.pin
@@ -7,6 +7,14 @@ import { base58 } from '@scure/base'
7
7
  import type { RelyOptions } from './types.js'
8
8
  import { RELY_CALL_TIMEOUT } from '@owlmeans/auth-common'
9
9
 
10
+ const toNumber = (bytes: Uint8Array): string => {
11
+ let pinInt = 0n
12
+ for (let i = bytes.length - 1; i >= 0; i--) {
13
+ pinInt = (pinInt << 8n) | BigInt(bytes[i]);
14
+ }
15
+ return pinInt.toString()
16
+ }
17
+
10
18
  export const makeRelyModel = (opts?: RelyOptions): EnvelopeModel<RelyToken> => {
11
19
  const envelope = makeEnvelopeModel<RelyToken>(RELY_3RD)
12
20
 
@@ -33,11 +41,3 @@ export const makeRelyModel = (opts?: RelyOptions): EnvelopeModel<RelyToken> => {
33
41
 
34
42
  return envelope
35
43
  }
36
-
37
- const toNumber = (bytes: Uint8Array): string => {
38
- let pinInt = 0n
39
- for (let i = bytes.length - 1; i >= 0; i--) {
40
- pinInt = (pinInt << 8n) | BigInt(bytes[i]);
41
- }
42
- return pinInt.toString()
43
- }
@@ -1,8 +1,10 @@
1
1
 
2
2
  import { makeRelyModel } from './peer.js'
3
+ import type { RelyToken } from '@owlmeans/auth'
4
+ import type { EnvelopeModel } from '@owlmeans/basic-envelope'
3
5
  import type { RelyOptions } from './types.js'
4
6
 
5
- export const makeProviderRely = (opts?: RelyOptions) => {
7
+ export const makeProviderRely = (opts?: RelyOptions): EnvelopeModel<RelyToken> => {
6
8
  const model = makeRelyModel(opts)
7
9
 
8
10
  return model
@@ -0,0 +1,52 @@
1
+ import { AUTH_SCOPE, AuthenticationType, AuthRole, GUEST_ID, RECAPTCHA_GUARD } from '@owlmeans/auth'
2
+ import type { Auth } from '@owlmeans/auth'
3
+ import { extractAuthToken } from '@owlmeans/auth-common/utils'
4
+ import { createService } from '@owlmeans/context'
5
+ import type { AbstractRequest, AbstractResponse, GuardService } from '@owlmeans/entrypoint'
6
+ import type { Config, Context } from '../types.local.js'
7
+ import { reCaptchaTokenOf } from './token.js'
8
+
9
+ /**
10
+ * The guard of a route a reCAPTCHA-proven guest may call once (`RECAPTCHA_GUARD`):
11
+ * `Authorization: RE-CAPTCHA <token>`, the token the auth manager's reCAPTCHA plugin issued.
12
+ *
13
+ * Every check of `ReCaptchaTokenHelper.inspect`, then the spend — a token admits one request. The
14
+ * request's `auth` is a guest (`GUEST_ID`, role Guest, `[AUTH_SCOPE]`, type ReCaptcha) with an empty
15
+ * `token`: whatever reads an authorization downstream may log it. A refused token is no credential at
16
+ * all, so the request is unauthenticated (401).
17
+ */
18
+ export const makeReCaptchaGuard = (alias: string = RECAPTCHA_GUARD): GuardService => {
19
+ const service: GuardService = createService<GuardService>(alias, {
20
+ match: async req => req != null && extractAuthToken(req, AuthenticationType.ReCaptcha) != null,
21
+
22
+ handle: async <T>(req: AbstractRequest, res: AbstractResponse<Auth>) => {
23
+ const context = service.assertCtx<Config, Context>()
24
+ const tokens = reCaptchaTokenOf(context)
25
+
26
+ const guest = await tokens.inspect(req)
27
+ if (guest == null || !await tokens.spend(guest)) {
28
+ return false as T
29
+ }
30
+
31
+ const auth: Auth = {
32
+ token: '',
33
+ type: AuthenticationType.ReCaptcha,
34
+ role: AuthRole.Guest,
35
+ userId: GUEST_ID,
36
+ scopes: [AUTH_SCOPE],
37
+ isUser: false,
38
+ source: context.cfg.service,
39
+ createdAt: new Date(),
40
+ expiresAt: guest.expiresAt,
41
+ }
42
+ res.resolve(auth)
43
+
44
+ return true as T
45
+ },
46
+
47
+ /** Server-side guard: it verifies what arrives, it never produces a credential. */
48
+ authenticated: async () => null,
49
+ })
50
+
51
+ return service
52
+ }
@@ -0,0 +1,3 @@
1
+ export type * from './types.js'
2
+ export * from './token.js'
3
+ export * from './guard.js'