@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.
- package/README.md +216 -38
- package/agent-meta/manifest.json +10 -3
- package/agent-meta/skills/server-auth/SKILL.md +161 -22
- package/agent-meta/skills/supervisor-auth/SKILL.md +128 -0
- package/build/actions/service.d.ts +6 -2
- package/build/actions/service.d.ts.map +1 -1
- package/build/actions/service.js +5 -4
- package/build/actions/service.js.map +1 -1
- package/build/actions/types.local.d.ts +6 -0
- package/build/actions/types.local.d.ts.map +1 -0
- package/build/actions/types.local.js +2 -0
- package/build/actions/types.local.js.map +1 -0
- package/build/entrypoints.d.ts +5 -0
- package/build/entrypoints.d.ts.map +1 -0
- package/build/entrypoints.js +9 -0
- package/build/entrypoints.js.map +1 -0
- package/build/index.d.ts +2 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -1
- package/build/index.js.map +1 -1
- package/build/manager/actions/authenticate.d.ts +8 -0
- package/build/manager/actions/authenticate.d.ts.map +1 -0
- package/build/manager/actions/authenticate.js +5 -0
- package/build/manager/actions/authenticate.js.map +1 -0
- package/build/manager/actions/index.d.ts +3 -1
- package/build/manager/actions/index.d.ts.map +1 -1
- package/build/manager/actions/index.js +3 -1
- package/build/manager/actions/index.js.map +1 -1
- package/build/manager/actions/init.d.ts +8 -0
- package/build/manager/actions/init.d.ts.map +1 -0
- package/build/manager/actions/init.js +5 -0
- package/build/manager/actions/init.js.map +1 -0
- package/build/manager/actions/rely.d.ts +8 -0
- package/build/manager/actions/rely.d.ts.map +1 -0
- package/build/manager/actions/rely.js +7 -0
- package/build/manager/actions/rely.js.map +1 -0
- package/build/manager/consts.local.d.ts +2 -0
- package/build/manager/consts.local.d.ts.map +1 -0
- package/build/manager/consts.local.js +2 -0
- package/build/manager/consts.local.js.map +1 -0
- package/build/manager/context.d.ts.map +1 -1
- package/build/manager/context.js +0 -1
- package/build/manager/context.js.map +1 -1
- package/build/manager/entrypoints.d.ts +9 -0
- package/build/manager/entrypoints.d.ts.map +1 -0
- package/build/manager/entrypoints.js +16 -0
- package/build/manager/entrypoints.js.map +1 -0
- package/build/manager/exports.d.ts +1 -2
- package/build/manager/exports.d.ts.map +1 -1
- package/build/manager/exports.js +1 -2
- package/build/manager/exports.js.map +1 -1
- package/build/manager/index.d.ts +1 -1
- package/build/manager/index.d.ts.map +1 -1
- package/build/manager/index.js +1 -1
- package/build/manager/index.js.map +1 -1
- package/build/manager/main.d.ts.map +1 -1
- package/build/manager/main.js +2 -2
- package/build/manager/main.js.map +1 -1
- package/build/manager/model.d.ts.map +1 -1
- package/build/manager/model.js +71 -32
- package/build/manager/model.js.map +1 -1
- package/build/manager/plugins/basic-ed25519.d.ts +1 -1
- package/build/manager/plugins/basic-ed25519.d.ts.map +1 -1
- package/build/manager/plugins/basic-ed25519.js +6 -6
- package/build/manager/plugins/basic-ed25519.js.map +1 -1
- package/build/manager/plugins/basic-rely.d.ts +1 -1
- package/build/manager/plugins/basic-rely.d.ts.map +1 -1
- package/build/manager/plugins/basic-rely.js +11 -8
- package/build/manager/plugins/basic-rely.js.map +1 -1
- package/build/manager/plugins/consts.d.ts +8 -0
- package/build/manager/plugins/consts.d.ts.map +1 -0
- package/build/manager/plugins/consts.js +9 -0
- package/build/manager/plugins/consts.js.map +1 -0
- package/build/manager/plugins/export.d.ts +5 -0
- package/build/manager/plugins/export.d.ts.map +1 -1
- package/build/manager/plugins/export.js +4 -0
- package/build/manager/plugins/export.js.map +1 -1
- package/build/manager/plugins/index.d.ts +3 -4
- package/build/manager/plugins/index.d.ts.map +1 -1
- package/build/manager/plugins/index.js +6 -6
- package/build/manager/plugins/index.js.map +1 -1
- package/build/manager/plugins/re-captcha/consts.d.ts +7 -0
- package/build/manager/plugins/re-captcha/consts.d.ts.map +1 -0
- package/build/manager/plugins/re-captcha/consts.js +7 -0
- package/build/manager/plugins/re-captcha/consts.js.map +1 -0
- package/build/manager/plugins/re-captcha/consts.local.d.ts +7 -0
- package/build/manager/plugins/re-captcha/consts.local.d.ts.map +1 -0
- package/build/manager/plugins/re-captcha/consts.local.js +9 -0
- package/build/manager/plugins/re-captcha/consts.local.js.map +1 -0
- package/build/manager/plugins/re-captcha/policy.d.ts +3 -0
- package/build/manager/plugins/re-captcha/policy.d.ts.map +1 -0
- package/build/manager/plugins/re-captcha/policy.js +44 -0
- package/build/manager/plugins/re-captcha/policy.js.map +1 -0
- package/build/manager/plugins/re-captcha/types.d.ts +79 -0
- package/build/manager/plugins/re-captcha/types.d.ts.map +1 -0
- package/build/manager/plugins/re-captcha/types.js +2 -0
- package/build/manager/plugins/re-captcha/types.js.map +1 -0
- package/build/manager/plugins/re-captcha/verifier.d.ts +12 -0
- package/build/manager/plugins/re-captcha/verifier.d.ts.map +1 -0
- package/build/manager/plugins/re-captcha/verifier.js +62 -0
- package/build/manager/plugins/re-captcha/verifier.js.map +1 -0
- package/build/manager/plugins/re-captcha.d.ts +10 -1
- package/build/manager/plugins/re-captcha.d.ts.map +1 -1
- package/build/manager/plugins/re-captcha.js +50 -37
- package/build/manager/plugins/re-captcha.js.map +1 -1
- package/build/manager/plugins/supervisor.d.ts +1 -2
- package/build/manager/plugins/supervisor.d.ts.map +1 -1
- package/build/manager/plugins/supervisor.js +6 -6
- package/build/manager/plugins/supervisor.js.map +1 -1
- package/build/manager/plugins/types.d.ts +22 -14
- package/build/manager/plugins/types.d.ts.map +1 -1
- package/build/manager/plugins/utils.d.ts +5 -4
- package/build/manager/plugins/utils.d.ts.map +1 -1
- package/build/manager/plugins/utils.js +20 -15
- package/build/manager/plugins/utils.js.map +1 -1
- package/build/manager/rely/flow.d.ts.map +1 -1
- package/build/manager/rely/flow.js +13 -11
- package/build/manager/rely/flow.js.map +1 -1
- package/build/manager/rely.d.ts.map +1 -1
- package/build/manager/rely.js.map +1 -1
- package/build/manager/supervisor.d.ts +4 -45
- package/build/manager/supervisor.d.ts.map +1 -1
- package/build/manager/supervisor.js +5 -5
- package/build/manager/supervisor.js.map +1 -1
- package/build/manager/types.d.ts +41 -1
- package/build/manager/types.d.ts.map +1 -1
- package/build/manager/types.local.d.ts +4 -0
- package/build/manager/types.local.d.ts.map +1 -0
- package/build/manager/types.local.js +2 -0
- package/build/manager/types.local.js.map +1 -0
- package/build/manager/utils/trusted.d.ts.map +1 -1
- package/build/manager/utils/trusted.js.map +1 -1
- package/build/model/rely/consumer.d.ts +3 -1
- package/build/model/rely/consumer.d.ts.map +1 -1
- package/build/model/rely/consumer.js.map +1 -1
- package/build/model/rely/peer.d.ts.map +1 -1
- package/build/model/rely/peer.js +7 -7
- package/build/model/rely/peer.js.map +1 -1
- package/build/model/rely/provider.d.ts +3 -1
- package/build/model/rely/provider.d.ts.map +1 -1
- package/build/model/rely/provider.js.map +1 -1
- package/build/re-captcha/guard.d.ts +12 -0
- package/build/re-captcha/guard.d.ts.map +1 -0
- package/build/re-captcha/guard.js +43 -0
- package/build/re-captcha/guard.js.map +1 -0
- package/build/re-captcha/index.d.ts +4 -0
- package/build/re-captcha/index.d.ts.map +1 -0
- package/build/re-captcha/index.js +3 -0
- package/build/re-captcha/index.js.map +1 -0
- package/build/re-captcha/token.d.ts +6 -0
- package/build/re-captcha/token.d.ts.map +1 -0
- package/build/re-captcha/token.js +74 -0
- package/build/re-captcha/token.js.map +1 -0
- package/build/re-captcha/types.d.ts +44 -0
- package/build/re-captcha/types.d.ts.map +1 -0
- package/build/re-captcha/types.js +2 -0
- package/build/re-captcha/types.js.map +1 -0
- package/build/service.d.ts +2 -3
- package/build/service.d.ts.map +1 -1
- package/build/service.js +136 -13
- package/build/service.js.map +1 -1
- package/build/types.local.d.ts +4 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/package.json +33 -30
- package/src/actions/service.ts +10 -12
- package/src/actions/types.local.ts +6 -0
- package/src/entrypoints.ts +12 -0
- package/src/index.ts +2 -2
- package/src/manager/README.md +14 -18
- package/src/manager/actions/authenticate.ts +12 -0
- package/src/manager/actions/index.ts +3 -1
- package/src/manager/actions/init.ts +12 -0
- package/src/manager/actions/rely.ts +12 -0
- package/src/manager/consts.local.ts +1 -0
- package/src/manager/context.ts +0 -2
- package/src/manager/entrypoints.ts +17 -0
- package/src/manager/exports.ts +1 -2
- package/src/manager/index.ts +1 -1
- package/src/manager/main.ts +2 -2
- package/src/manager/model.ts +82 -41
- package/src/manager/plugins/basic-ed25519.ts +8 -8
- package/src/manager/plugins/basic-rely.ts +14 -9
- package/src/manager/plugins/consts.ts +7 -0
- package/src/manager/plugins/export.ts +5 -0
- package/src/manager/plugins/index.ts +10 -12
- package/src/manager/plugins/re-captcha/consts.local.ts +8 -0
- package/src/manager/plugins/re-captcha/consts.ts +8 -0
- package/src/manager/plugins/re-captcha/policy.ts +56 -0
- package/src/manager/plugins/re-captcha/types.ts +85 -0
- package/src/manager/plugins/re-captcha/verifier.ts +74 -0
- package/src/manager/plugins/re-captcha.ts +58 -43
- package/src/manager/plugins/supervisor.ts +9 -11
- package/src/manager/plugins/types.ts +23 -14
- package/src/manager/plugins/utils.ts +24 -14
- package/src/manager/rely/flow.ts +14 -11
- package/src/manager/rely.ts +2 -5
- package/src/manager/supervisor.ts +8 -54
- package/src/manager/types.local.ts +5 -0
- package/src/manager/types.ts +46 -1
- package/src/model/rely/consumer.ts +3 -1
- package/src/model/rely/peer.ts +8 -8
- package/src/model/rely/provider.ts +3 -1
- package/src/re-captcha/guard.ts +52 -0
- package/src/re-captcha/index.ts +3 -0
- package/src/re-captcha/token.ts +89 -0
- package/src/re-captcha/types.ts +45 -0
- package/src/service.ts +133 -19
- package/src/types.local.ts +5 -0
- package/tests/context.ts +3 -2
- package/tests/re-captcha.spec.ts +298 -0
- package/tests/replay-policy.spec.ts +36 -0
- package/tests/supervisor.spec.ts +6 -4
- package/build/.gitkeep +0 -0
- package/src/manager/actions/manager.ts +0 -23
- package/src/manager/modules.ts +0 -29
- package/src/modules.ts +0 -17
|
@@ -1,59 +1,74 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
return { challenge }
|
|
58
|
+
return { challenge: base64.encode(randomBytes(32)) }
|
|
23
59
|
},
|
|
24
60
|
|
|
25
61
|
authenticate: async credential => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
47
|
-
|
|
48
|
-
credential.type = AuthenticationType.ReCaptcha
|
|
66
|
+
const answer = await verifier().verify({ secret, response: credential.credential })
|
|
67
|
+
policy.assert(answer)
|
|
49
68
|
|
|
50
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
75
|
-
credential.
|
|
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
|
|
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
|
|
19
|
-
secret: string
|
|
20
|
-
response: string
|
|
21
|
-
remoteip?: string
|
|
22
|
-
}> { }
|
|
19
|
+
export interface AuthRedisResource extends RedisResource<RelyRecord> {}
|
|
23
20
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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)
|
package/src/manager/rely/flow.ts
CHANGED
|
@@ -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 => {
|
|
44
|
-
conn._receiveResult = async msg => {
|
|
45
|
-
conn._receiveError = async msg => {
|
|
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
|
-
|
|
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
|
-
|
|
124
|
+
log.warn('Rely authentication failed; connection closed', e)
|
|
122
125
|
await conn.close()
|
|
123
126
|
}
|
|
124
127
|
break
|
package/src/manager/rely.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
20
|
+
entrypoints: Array<{ guards?: string[] }>, guard: string = DEFAULT_GUARD
|
|
67
21
|
): void => {
|
|
68
|
-
|
|
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
|
|
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
|
|
54
|
+
(ctx => makeSupervisorPlugin(ctx as unknown as AppContext<AppConfig>, resolved)) as AuthPluginFactory
|
|
101
55
|
)
|
|
102
56
|
|
|
103
57
|
if (opts?.acceptInternalTokens !== false) {
|
package/src/manager/types.ts
CHANGED
|
@@ -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
|
package/src/model/rely/peer.ts
CHANGED
|
@@ -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
|
+
}
|