@owlmeans/server-auth-otp 0.1.18-rc.4 → 0.1.18-rc.40
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 +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-auth-otp/SKILL.md +82 -21
- package/build/challenge.d.ts +8 -0
- package/build/challenge.d.ts.map +1 -0
- package/build/challenge.js +98 -0
- package/build/challenge.js.map +1 -0
- package/build/consts.d.ts +10 -1
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +7 -1
- package/build/consts.js.map +1 -1
- package/build/errors.d.ts +18 -0
- package/build/errors.d.ts.map +1 -0
- package/build/errors.js +32 -0
- package/build/errors.js.map +1 -0
- package/build/index.d.ts +4 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -0
- package/build/index.js.map +1 -1
- package/build/plugin.d.ts.map +1 -1
- package/build/plugin.js +18 -17
- package/build/plugin.js.map +1 -1
- package/build/service.d.ts +3 -0
- package/build/service.d.ts.map +1 -1
- package/build/service.js +45 -22
- package/build/service.js.map +1 -1
- package/build/throttle.d.ts +12 -0
- package/build/throttle.d.ts.map +1 -0
- package/build/throttle.js +93 -0
- package/build/throttle.js.map +1 -0
- package/build/types.d.ts +34 -0
- package/build/types.d.ts.map +1 -1
- package/build/types.js +7 -1
- package/build/types.js.map +1 -1
- package/package.json +13 -13
- package/src/challenge.ts +114 -0
- package/src/consts.ts +11 -1
- package/src/errors.ts +37 -0
- package/src/index.ts +4 -1
- package/src/plugin.ts +18 -17
- package/src/service.ts +59 -34
- package/src/throttle.ts +120 -0
- package/src/types.ts +42 -0
- package/tests/context.ts +43 -0
- package/tests/keys.spec.ts +23 -0
- package/tests/service.spec.ts +88 -0
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Email OTP `AuthPlugin` for OwlMeans servers — passwordless login via time-limi
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
bun add @owlmeans/server-auth-otp
|
|
15
|
+
bun add @owlmeans/server-auth-otp@^0.1.18-rc.40
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
## Usage
|
|
@@ -47,7 +47,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
47
47
|
your project's skill store (`.agents/skills/`):
|
|
48
48
|
|
|
49
49
|
```sh
|
|
50
|
-
npx @owlmeans/agent-skills
|
|
50
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.34
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/server-auth-otp",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.40",
|
|
5
|
+
"generatedAt": "2026-09-22T20:45:24.051Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,32 +8,50 @@ metadata:
|
|
|
8
8
|
|
|
9
9
|
# Using `@owlmeans/server-auth-otp`
|
|
10
10
|
|
|
11
|
+
**Install:** `"@owlmeans/server-auth-otp": "^0.1.18-rc.40"` in `dependencies`
|
|
12
|
+
|
|
11
13
|
Email OTP authentication plugin for the OwlMeans auth-manager plugin system. Relies on `@owlmeans/auth-otp` for the OTP service interface, a Redis resource for code storage, and a `MailerService` to send codes.
|
|
12
14
|
|
|
15
|
+
## `@owlmeans/auth-otp` — the contracts
|
|
16
|
+
|
|
17
|
+
`@owlmeans/auth-otp` has no skill of its own because it is the contracts half of this pair. Its
|
|
18
|
+
`OtpService` issues an opaque issuance id, persists and mails a code, and verifies that exact
|
|
19
|
+
issuance; it also owns `OTP_SERVICE`, `OTP_AUTH_TYPE` (`'email-otp'`), `OTP_RESOURCE`,
|
|
20
|
+
`OTP_TTL_SECONDS` (600), `OTP_CODE_LENGTH` (6), and the five-attempt policy. The server package
|
|
21
|
+
provides the challenge stores, throttles, mailer integration and plugin.
|
|
22
|
+
|
|
23
|
+
Depend on it from a shared package that must name the auth type or type the service, and depend on
|
|
24
|
+
`@owlmeans/server-auth-otp` only where the server wires itself up — the same producer/consumer split
|
|
25
|
+
every other contracts-and-driver pair in the framework uses. Add a constant or a method signature to
|
|
26
|
+
the contracts package, never to the implementation.
|
|
27
|
+
|
|
13
28
|
## Public API surface
|
|
14
29
|
|
|
15
30
|
| Symbol | Kind | Purpose |
|
|
16
31
|
|--------|------|---------|
|
|
17
32
|
| `makeOtpService(alias?)` | fn | Service factory — stores/verifies OTP codes |
|
|
18
33
|
| `appendOtpPlugin(context)` | fn | Registers the OTP `AuthPlugin` into the server-auth plugin registry |
|
|
34
|
+
| `OTP_SERVICE` | const | `'auth-otp-service'` — the service alias `makeOtpService` registers under |
|
|
19
35
|
| `OTP_AUTH_TYPE` | const | `'email-otp'` — the auth type string to pass in `init` requests |
|
|
20
36
|
| `OTP_RESOURCE` | const | Redis resource alias for code storage |
|
|
21
37
|
| `OTP_TTL_SECONDS` | const | Code TTL (600 s = 10 min) |
|
|
22
38
|
| `OTP_CODE_LENGTH` | const | 6 |
|
|
39
|
+
| `SERVER_AUTH_OTP` | const | `'server-auth-otp'` — this package's own alias |
|
|
40
|
+
| `OtpConfig`, `OtpContext` | type | The `cfg.otp` overrides below, as a server config/context |
|
|
23
41
|
|
|
24
42
|
## Registration requirements
|
|
25
43
|
|
|
26
44
|
```ts
|
|
27
45
|
import { makeOtpService, appendOtpPlugin, OTP_RESOURCE } from '@owlmeans/server-auth-otp'
|
|
28
46
|
import { makeRedisResource } from '@owlmeans/redis-resource'
|
|
29
|
-
import {
|
|
47
|
+
import { makeDefaultConsoleMailerService, MAILER_SERVICE } from '@owlmeans/mailer'
|
|
30
48
|
import { makeMailgunMailerService } from '@owlmeans/server-mailer-mailgun'
|
|
31
49
|
|
|
32
50
|
// 1. Register the Redis code-cache resource.
|
|
33
51
|
context.registerResource(makeRedisResource(OTP_RESOURCE))
|
|
34
52
|
|
|
35
|
-
// 2. Register a MailerService (console for dev/tests, Mailgun for prod).
|
|
36
|
-
context.registerService(
|
|
53
|
+
// 2. Register a MailerService under MAILER_SERVICE (console for dev/tests, Mailgun for prod).
|
|
54
|
+
context.registerService(makeDefaultConsoleMailerService())
|
|
37
55
|
// or:
|
|
38
56
|
context.registerService(makeMailgunMailerService(MAILER_SERVICE))
|
|
39
57
|
|
|
@@ -48,13 +66,20 @@ appendOtpPlugin(context)
|
|
|
48
66
|
|
|
49
67
|
**Init** — client sends `{ type: 'email-otp', userId: 'user@email.com' }`:
|
|
50
68
|
- OTP service generates a 6-digit code, stores it in Redis with 10 min TTL, emails it.
|
|
51
|
-
- Returns
|
|
52
|
-
|
|
53
|
-
**Authenticate** — client sends `{ challenge: <signed-envelope>, userId: email, credential: '123456',
|
|
54
|
-
- Envelope is opened →
|
|
55
|
-
|
|
56
|
-
- `IdentityLinkingService` finds
|
|
57
|
-
|
|
69
|
+
- Returns an opaque issuance id in a signed envelope. The id never contains the email or code.
|
|
70
|
+
|
|
71
|
+
**Authenticate** — client sends `{ challenge: <signed-envelope>, userId: email, credential: '123456', type: 'email-otp', role: AuthRole.User, scopes: [ALL_SCOPES] }`:
|
|
72
|
+
- Envelope is opened → the plugin verifies the opaque issuance and code atomically. Five failed
|
|
73
|
+
attempts invalidate it; one concurrent correct verification can consume it.
|
|
74
|
+
- `IdentityLinkingService` finds the linked profile, or links this email to the person's platform
|
|
75
|
+
identity — registering an account, a profile and an organization entity only when the address is
|
|
76
|
+
new to the platform.
|
|
77
|
+
- Copies `userId`, `profileId`, `entitySlug`, `role` and `scopes` from the resolved payload onto the
|
|
78
|
+
credential, sets `credential.type = AuthenticationType.OneTimeToken`, and returns the signed auth
|
|
79
|
+
token.
|
|
80
|
+
- `type`, `role`, `scopes` are required by the shared `AuthCredentialsSchema` (spread from
|
|
81
|
+
`AuthPayloadSchema.required`) even though the OTP plugin overwrites `role`/`scopes`/`type` on
|
|
82
|
+
success — a caller that omits them never reaches the plugin at all (see Gotchas).
|
|
58
83
|
|
|
59
84
|
## Config overrides (optional)
|
|
60
85
|
|
|
@@ -70,16 +95,52 @@ cfg.otp = {
|
|
|
70
95
|
|
|
71
96
|
## Rules
|
|
72
97
|
|
|
73
|
-
- Always register the Redis
|
|
98
|
+
- Always register the Redis challenge store, Redis throttle service, and mailer service BEFORE the
|
|
99
|
+
OTP service. Production uses `makeRedisOtpChallengeStore` and `makeRedisThrottleService`; memory
|
|
100
|
+
implementations are local/test defaults only.
|
|
74
101
|
- Call `appendOtpPlugin(context)` once per context — it adds to the shared plugin registry singleton.
|
|
75
|
-
-
|
|
102
|
+
- `credential.entitySlug` on the authenticate request **selects nothing**. The plugin copies it into
|
|
103
|
+
the linking details as `clientId` (defaulting to `'default'`) and `entityId`, and
|
|
104
|
+
`@owlmeans/server-auth-identity` reads neither: `getLinkedProfile` keys on the external login key
|
|
105
|
+
built from the auth type, the `'email'` service and the address, and `linkProfile` either reuses
|
|
106
|
+
the person's existing platform profile — matched on the account name — or mints a brand-new
|
|
107
|
+
organization entity. Whatever the caller sent is then overwritten with the linked profile's own
|
|
108
|
+
slug before the envelope is signed, so the address alone decides which identity and which
|
|
109
|
+
organization the token names.
|
|
76
110
|
- Errors from this plugin are `AuthenFailed` (from `@owlmeans/auth`) — callers catch that, not raw `Error`.
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
111
|
+
- Use the email throttle for the public integrated-IAM flow: one issuance per minute and ten per
|
|
112
|
+
hour, with a 429 and Retry-After. The companion IP throttle is available for an ingress that
|
|
113
|
+
safely supplies a normalized client address; throttle keys hash email/IP values and never store
|
|
114
|
+
the raw identifier.
|
|
115
|
+
- For tests, register `makeDefaultConsoleMailerService()` and read `svc.captured[n].text` to extract
|
|
116
|
+
the code. Bare `makeConsoleMailerService()` registers under `CONSOLE_MAILER` (`'console-mailer'`),
|
|
117
|
+
which is not the alias `makeOtpService` resolves — it looks up `cfg.otp?.mailerAlias ?? MAILER_SERVICE`.
|
|
118
|
+
|
|
119
|
+
## Gotchas
|
|
120
|
+
|
|
121
|
+
- **The OTP challenge must be opaque and plugin-owned.** The generic auth-manager replay policy
|
|
122
|
+
does not consume it before the OTP store can count attempts; the plugin atomically owns verify
|
|
123
|
+
and consume. Never encode the email or code in the issuance id, and never replace the bounded
|
|
124
|
+
attempt counter with a bare auth-cache replay key.
|
|
125
|
+
- **`AuthCredentialsSchema.credential` has a `minLength` floor** (from `@owlmeans/auth`) sized for
|
|
126
|
+
long tokens/signatures from other plugins (Ed25519 signature, OAuth code). A 6-digit OTP code is
|
|
127
|
+
legitimately shorter — the floor is `minLength: 1` and must stay low enough to admit it, or every
|
|
128
|
+
authenticate call 400s before the plugin ever runs.
|
|
129
|
+
- **`scopes`/`role`/`type` are schema-required on the authenticate body**, spread from
|
|
130
|
+
`AuthPayloadSchema.required` into `AuthCredentialsSchema` — even though this plugin overwrites
|
|
131
|
+
all three on success. A caller built without going through `@owlmeans/client-auth`'s
|
|
132
|
+
`AuthenticationControl` (which fills them automatically) must set them explicitly or the request
|
|
133
|
+
fails Fastify schema validation (`FST_ERR_VALIDATION`) before reaching this plugin at all.
|
|
134
|
+
- **The resulting auth token can exceed 1024 characters** — it wraps the full `AuthCredentials`
|
|
135
|
+
envelope, including the original allowance challenge, base64-encoded. A consumer route that
|
|
136
|
+
accepts this token (e.g. an OIDC `PROVIDER_INTERACTION` finalizer) must size its own `token`
|
|
137
|
+
field schema accordingly; the generic `AuthTokenSchema` (`maxLength: 1024`) is too small.
|
|
138
|
+
|
|
139
|
+
## Related
|
|
140
|
+
|
|
141
|
+
- `@owlmeans/auth-otp` — the `OtpService` interface and the shared constants
|
|
142
|
+
- `@owlmeans/mailer` — `MailerService`, the console transport
|
|
143
|
+
- `@owlmeans/server-mailer-mailgun` — the production Mailgun transport
|
|
144
|
+
- `server-auth` skill — the auth-manager plugin system this plugin registers into
|
|
145
|
+
- `server-auth-identity` skill — how the linked profile and its organization entity are stored
|
|
146
|
+
- `auth-protocol` skill — error hierarchy and identity read rules
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OtpChallengeStore } from './types.js';
|
|
2
|
+
type Clock = () => number;
|
|
3
|
+
/** Real in-memory challenge store with synchronous consume/update semantics. */
|
|
4
|
+
export declare const makeMemoryOtpChallengeStore: (alias?: string, now?: Clock) => OtpChallengeStore;
|
|
5
|
+
/** Redis challenge store: compare, attempt increment, exhaustion, and consume are one operation. */
|
|
6
|
+
export declare const makeRedisOtpChallengeStore: (alias?: string, resourceAlias?: string) => OtpChallengeStore;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=challenge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge.d.ts","sourceRoot":"","sources":["../src/challenge.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACyB,iBAAiB,EACrD,MAAM,YAAY,CAAA;AAGnB,KAAK,KAAK,GAAG,MAAM,MAAM,CAAA;AAGzB,gFAAgF;AAChF,eAAO,MAAM,2BAA2B,yBACJ,KAAK,KACtC,iBA+BF,CAAA;AA0BD,oGAAoG;AACpG,eAAO,MAAM,0BAA0B,8CAEpC,iBAqCF,CAAA"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { appendContextual, createService } from '@owlmeans/context';
|
|
2
|
+
import { OtpUnavailable } from './errors.js';
|
|
3
|
+
import { OTP_CHALLENGE_STORE, OTP_RESOURCE } from './consts.js';
|
|
4
|
+
import { OtpChallengeOutcome as Outcome } from './types.js';
|
|
5
|
+
/** Real in-memory challenge store with synchronous consume/update semantics. */
|
|
6
|
+
export const makeMemoryOtpChallengeStore = (alias = OTP_CHALLENGE_STORE, now = Date.now) => {
|
|
7
|
+
const records = new Map();
|
|
8
|
+
return createService(alias, {
|
|
9
|
+
issue: async (challenge, ttlSeconds) => {
|
|
10
|
+
const current = records.get(challenge.id);
|
|
11
|
+
if (current != null && current.expiresAt > now())
|
|
12
|
+
return false;
|
|
13
|
+
records.set(challenge.id, {
|
|
14
|
+
...challenge, failedAttempts: 0, expiresAt: now() + ttlSeconds * 1000,
|
|
15
|
+
});
|
|
16
|
+
return true;
|
|
17
|
+
},
|
|
18
|
+
verify: async (id, emailKey, codeHash, maxFailedAttempts) => {
|
|
19
|
+
const record = records.get(id);
|
|
20
|
+
if (record == null || record.expiresAt <= now()) {
|
|
21
|
+
records.delete(id);
|
|
22
|
+
return Outcome.Missing;
|
|
23
|
+
}
|
|
24
|
+
if (record.emailKey === emailKey && record.codeHash === codeHash) {
|
|
25
|
+
records.delete(id);
|
|
26
|
+
return Outcome.Verified;
|
|
27
|
+
}
|
|
28
|
+
record.failedAttempts += 1;
|
|
29
|
+
if (record.failedAttempts >= maxFailedAttempts) {
|
|
30
|
+
records.delete(id);
|
|
31
|
+
return Outcome.Exhausted;
|
|
32
|
+
}
|
|
33
|
+
return Outcome.Invalid;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const REDIS_VERIFY_SCRIPT = `
|
|
38
|
+
local raw = redis.call('GET', KEYS[1])
|
|
39
|
+
if not raw then return 0 end
|
|
40
|
+
local record = cjson.decode(raw)
|
|
41
|
+
if record.emailKey == ARGV[1] and record.codeHash == ARGV[2] then
|
|
42
|
+
redis.call('DEL', KEYS[1])
|
|
43
|
+
return 1
|
|
44
|
+
end
|
|
45
|
+
record.failedAttempts = tonumber(record.failedAttempts or 0) + 1
|
|
46
|
+
if record.failedAttempts >= tonumber(ARGV[3]) then
|
|
47
|
+
redis.call('DEL', KEYS[1])
|
|
48
|
+
return -2
|
|
49
|
+
end
|
|
50
|
+
local ttl = redis.call('PTTL', KEYS[1])
|
|
51
|
+
if ttl <= 0 then
|
|
52
|
+
redis.call('DEL', KEYS[1])
|
|
53
|
+
return 0
|
|
54
|
+
end
|
|
55
|
+
redis.call('SET', KEYS[1], cjson.encode(record), 'XX', 'PX', ttl)
|
|
56
|
+
return -1
|
|
57
|
+
`;
|
|
58
|
+
/** Redis challenge store: compare, attempt increment, exhaustion, and consume are one operation. */
|
|
59
|
+
export const makeRedisOtpChallengeStore = (alias = OTP_CHALLENGE_STORE, resourceAlias = OTP_RESOURCE) => {
|
|
60
|
+
const service = appendContextual(alias, {
|
|
61
|
+
issue: async (challenge, ttlSeconds) => {
|
|
62
|
+
const ctx = service.ctx;
|
|
63
|
+
const resource = ctx.resource(resourceAlias);
|
|
64
|
+
try {
|
|
65
|
+
const result = await resource.db.client.set(resource.key(challenge.id), JSON.stringify({ ...challenge, failedAttempts: 0 }), 'EX', ttlSeconds, 'NX');
|
|
66
|
+
return result === 'OK';
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const unavailable = new OtpUnavailable('challenge');
|
|
70
|
+
if (error instanceof Error)
|
|
71
|
+
unavailable.oiriginalStack = `${error}: ${error.stack}`;
|
|
72
|
+
throw unavailable;
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
verify: async (id, emailKey, codeHash, maxFailedAttempts) => {
|
|
76
|
+
const ctx = service.ctx;
|
|
77
|
+
const resource = ctx.resource(resourceAlias);
|
|
78
|
+
try {
|
|
79
|
+
const result = Number(await resource.db.client.eval(REDIS_VERIFY_SCRIPT, 1, resource.key(id), emailKey, codeHash, String(maxFailedAttempts)));
|
|
80
|
+
if (result === 1)
|
|
81
|
+
return Outcome.Verified;
|
|
82
|
+
if (result === -1)
|
|
83
|
+
return Outcome.Invalid;
|
|
84
|
+
if (result === -2)
|
|
85
|
+
return Outcome.Exhausted;
|
|
86
|
+
return Outcome.Missing;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const unavailable = new OtpUnavailable('challenge');
|
|
90
|
+
if (error instanceof Error)
|
|
91
|
+
unavailable.oiriginalStack = `${error}: ${error.stack}`;
|
|
92
|
+
throw unavailable;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
return service;
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=challenge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge.js","sourceRoot":"","sources":["../src/challenge.ts"],"names":[],"mappings":"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"}
|
package/build/consts.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export declare const SERVER_AUTH_OTP = "server-auth-otp";
|
|
2
|
+
export declare const OTP_CHALLENGE_STORE = "auth-otp-challenge-store";
|
|
3
|
+
export declare const OTP_THROTTLE_SERVICE = "auth-otp-throttle";
|
|
4
|
+
export declare const OTP_EMAIL_THROTTLE_RULES: readonly (Readonly<{
|
|
5
|
+
limit: 1;
|
|
6
|
+
windowSeconds: 60;
|
|
7
|
+
}> | Readonly<{
|
|
8
|
+
limit: 10;
|
|
9
|
+
windowSeconds: number;
|
|
10
|
+
}>)[];
|
|
2
11
|
/** The OTP auth plugin type key — passed as credential.type in init requests */
|
|
3
|
-
export { OTP_AUTH_TYPE, OTP_SERVICE, OTP_RESOURCE, OTP_TTL_SECONDS, OTP_CODE_LENGTH } from '@owlmeans/auth-otp';
|
|
12
|
+
export { OTP_AUTH_TYPE, OTP_SERVICE, OTP_RESOURCE, OTP_TTL_SECONDS, OTP_CODE_LENGTH, OTP_MAX_FAILED_ATTEMPTS, } from '@owlmeans/auth-otp';
|
|
4
13
|
//# sourceMappingURL=consts.d.ts.map
|
package/build/consts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,oBAAoB,CAAA;AAChD,eAAO,MAAM,mBAAmB,6BAA6B,CAAA;AAC7D,eAAO,MAAM,oBAAoB,sBAAsB,CAAA;AAEvD,eAAO,MAAM,wBAAwB;;;;;;KAGnC,CAAA;AAEF,gFAAgF;AAChF,OAAO,EACL,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAC1E,uBAAuB,GACxB,MAAM,oBAAoB,CAAA"}
|
package/build/consts.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export const SERVER_AUTH_OTP = 'server-auth-otp';
|
|
2
|
+
export const OTP_CHALLENGE_STORE = 'auth-otp-challenge-store';
|
|
3
|
+
export const OTP_THROTTLE_SERVICE = 'auth-otp-throttle';
|
|
4
|
+
export const OTP_EMAIL_THROTTLE_RULES = Object.freeze([
|
|
5
|
+
Object.freeze({ limit: 1, windowSeconds: 60 }),
|
|
6
|
+
Object.freeze({ limit: 10, windowSeconds: 60 * 60 }),
|
|
7
|
+
]);
|
|
2
8
|
/** The OTP auth plugin type key — passed as credential.type in init requests */
|
|
3
|
-
export { OTP_AUTH_TYPE, OTP_SERVICE, OTP_RESOURCE, OTP_TTL_SECONDS, OTP_CODE_LENGTH } from '@owlmeans/auth-otp';
|
|
9
|
+
export { OTP_AUTH_TYPE, OTP_SERVICE, OTP_RESOURCE, OTP_TTL_SECONDS, OTP_CODE_LENGTH, OTP_MAX_FAILED_ATTEMPTS, } from '@owlmeans/auth-otp';
|
|
4
10
|
//# sourceMappingURL=consts.js.map
|
package/build/consts.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAA;AAChD,MAAM,CAAC,MAAM,mBAAmB,GAAG,0BAA0B,CAAA;AAC7D,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAA;AAEvD,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC,MAAM,CAAC;IACpD,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC;IAC9C,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC;CACrD,CAAC,CAAA;AAEF,gFAAgF;AAChF,OAAO,EACL,aAAa,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAC1E,uBAAuB,GACxB,MAAM,oBAAoB,CAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ResilientError } from '@owlmeans/error';
|
|
2
|
+
export declare class OtpError extends ResilientError {
|
|
3
|
+
static typeName: string;
|
|
4
|
+
constructor(message?: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class OtpThrottled extends OtpError {
|
|
7
|
+
static typeName: string;
|
|
8
|
+
static httpStatus: number;
|
|
9
|
+
readonly retryAfter: number;
|
|
10
|
+
constructor(retryAfter?: number | string, stack?: string);
|
|
11
|
+
}
|
|
12
|
+
export declare class OtpUnavailable extends OtpError {
|
|
13
|
+
static typeName: string;
|
|
14
|
+
static httpStatus: number;
|
|
15
|
+
static allowServerErrorStatus: boolean;
|
|
16
|
+
constructor(message?: string);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,qBAAa,QAAS,SAAQ,cAAc;IAC1C,OAAuB,QAAQ,SAAa;IAE5C,YAAY,OAAO,GAAE,MAAgB,EAEpC;CACF;AAED,qBAAa,YAAa,SAAQ,QAAQ;IACxC,OAAuB,QAAQ,SAAkC;IACjE,OAAc,UAAU,SAAM;IAC9B,SAAgB,UAAU,EAAE,MAAM,CAAA;IAElC,YAAY,UAAU,GAAE,MAAM,GAAG,MAAU,EAAE,KAAK,CAAC,EAAE,MAAM,EAK1D;CACF;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,OAAuB,QAAQ,SAAoC;IACnE,OAAc,UAAU,SAAM;IAC9B,OAAc,sBAAsB,UAAO;IAE3C,YAAY,OAAO,GAAE,MAAgB,EAGpC;CACF"}
|
package/build/errors.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ResilientError } from '@owlmeans/error';
|
|
2
|
+
export class OtpError extends ResilientError {
|
|
3
|
+
static typeName = 'OtpError';
|
|
4
|
+
constructor(message = 'error') {
|
|
5
|
+
super(OtpError.typeName, `otp:${message}`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class OtpThrottled extends OtpError {
|
|
9
|
+
static typeName = `${OtpError.typeName}Throttled`;
|
|
10
|
+
static httpStatus = 429;
|
|
11
|
+
retryAfter;
|
|
12
|
+
constructor(retryAfter = 1, stack) {
|
|
13
|
+
super(typeof retryAfter === 'string' ? retryAfter : 'throttled');
|
|
14
|
+
this.type = OtpThrottled.typeName;
|
|
15
|
+
this.retryAfter = typeof retryAfter === 'number' ? Math.max(1, Math.ceil(retryAfter)) : 1;
|
|
16
|
+
if (stack != null)
|
|
17
|
+
this.oiriginalStack = stack;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class OtpUnavailable extends OtpError {
|
|
21
|
+
static typeName = `${OtpError.typeName}Unavailable`;
|
|
22
|
+
static httpStatus = 503;
|
|
23
|
+
static allowServerErrorStatus = true;
|
|
24
|
+
constructor(message = 'store') {
|
|
25
|
+
super(`unavailable:${message}`);
|
|
26
|
+
this.type = OtpUnavailable.typeName;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
ResilientError.registerErrorClass(OtpError);
|
|
30
|
+
ResilientError.registerErrorClass(OtpThrottled);
|
|
31
|
+
ResilientError.registerErrorClass(OtpUnavailable);
|
|
32
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,OAAO,QAAS,SAAQ,cAAc;IACnC,MAAM,CAAU,QAAQ,GAAG,UAAU,CAAA;IAE5C,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,OAAO,EAAE,CAAC,CAAA;IAC5C,CAAC;CACF;AAED,MAAM,OAAO,YAAa,SAAQ,QAAQ;IACjC,MAAM,CAAU,QAAQ,GAAG,GAAG,QAAQ,CAAC,QAAQ,WAAW,CAAA;IAC1D,MAAM,CAAC,UAAU,GAAG,GAAG,CAAA;IACd,UAAU,CAAQ;IAElC,YAAY,UAAU,GAAoB,CAAC,EAAE,KAAc;QACzD,KAAK,CAAC,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAA;QAChE,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC,QAAQ,CAAA;QACjC,IAAI,CAAC,UAAU,GAAG,OAAO,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QACzF,IAAI,KAAK,IAAI,IAAI;YAAE,IAAI,CAAC,cAAc,GAAG,KAAK,CAAA;IAChD,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,QAAQ;IACnC,MAAM,CAAU,QAAQ,GAAG,GAAG,QAAQ,CAAC,QAAQ,aAAa,CAAA;IAC5D,MAAM,CAAC,UAAU,GAAG,GAAG,CAAA;IACvB,MAAM,CAAC,sBAAsB,GAAG,IAAI,CAAA;IAE3C,YAAY,OAAO,GAAW,OAAO;QACnC,KAAK,CAAC,eAAe,OAAO,EAAE,CAAC,CAAA;QAC/B,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC,QAAQ,CAAA;IACrC,CAAC;CACF;AAED,cAAc,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAA;AAC3C,cAAc,CAAC,kBAAkB,CAAC,YAAY,CAAC,CAAA;AAC/C,cAAc,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAA"}
|
package/build/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
export
|
|
1
|
+
export * from './types.js';
|
|
2
2
|
export * from './consts.js';
|
|
3
3
|
export * from './service.js';
|
|
4
4
|
export * from './plugin.js';
|
|
5
|
+
export * from './errors.js';
|
|
6
|
+
export * from './throttle.js';
|
|
7
|
+
export * from './challenge.js';
|
|
5
8
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA"}
|
package/build/index.js
CHANGED
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA"}
|
package/build/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAqEvD,sGAAsG;AACtG,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,KAAG,CAG1F,CAAA"}
|
package/build/plugin.js
CHANGED
|
@@ -1,47 +1,48 @@
|
|
|
1
1
|
import { ALL_SCOPES, AuthenFailed, AuthRole, AuthenticationType } from '@owlmeans/auth';
|
|
2
|
-
import { registerPlugin } from '@owlmeans/server-auth/manager/plugins';
|
|
2
|
+
import { AuthChallengeReplayPolicy, registerPlugin } from '@owlmeans/server-auth/manager/plugins';
|
|
3
3
|
import { OTP_AUTH_TYPE, OTP_SERVICE } from '@owlmeans/auth-otp';
|
|
4
4
|
import { AUTH_IDENTITY_LINKING } from '@owlmeans/server-auth-identity';
|
|
5
|
-
|
|
6
|
-
// Separates the email from the per-request nonce inside the challenge (see init() below).
|
|
5
|
+
// Separates the email from the opaque issuance id inside the signed challenge.
|
|
7
6
|
const CHALLENGE_DELIMITER = '::';
|
|
8
7
|
/** Factory that creates the email-OTP AuthPlugin bound to the given context. */
|
|
9
8
|
const otpPlugin = (context) => ({
|
|
10
9
|
type: OTP_AUTH_TYPE,
|
|
10
|
+
challengeReplayPolicy: AuthChallengeReplayPolicy.Plugin,
|
|
11
11
|
init: async (request) => {
|
|
12
12
|
if (request.userId == null || !request.userId.includes('@')) {
|
|
13
13
|
throw new AuthenFailed('otp:email');
|
|
14
14
|
}
|
|
15
15
|
const email = request.userId.trim().toLowerCase();
|
|
16
16
|
const otp = context.service(OTP_SERVICE);
|
|
17
|
-
await otp.issueChallenge(email);
|
|
18
|
-
|
|
19
|
-
// must be unique per request — the plaintext email alone would be identical across every
|
|
20
|
-
// independent login attempt for that address and collide with a still-cached prior one.
|
|
21
|
-
const nonce = createIdOfLength(16, IdStyle.Base58);
|
|
22
|
-
return { challenge: `${email}${CHALLENGE_DELIMITER}${nonce}` };
|
|
17
|
+
const issuanceId = await otp.issueChallenge(email);
|
|
18
|
+
return { challenge: `${email}${CHALLENGE_DELIMITER}${issuanceId}` };
|
|
23
19
|
},
|
|
24
20
|
authenticate: async (credential) => {
|
|
25
|
-
// credential.challenge is "email::
|
|
26
|
-
const
|
|
21
|
+
// credential.challenge is "email::issuanceId" (opened from the signed envelope).
|
|
22
|
+
const parts = credential.challenge?.split(CHALLENGE_DELIMITER) ?? [];
|
|
23
|
+
const email = parts.length === 2 ? parts[0] : undefined;
|
|
24
|
+
const issuanceId = parts.length === 2 ? parts[1] : undefined;
|
|
27
25
|
const code = credential.credential;
|
|
28
|
-
if (!email || !email.includes('@')) {
|
|
26
|
+
if (!email || !email.includes('@') || !issuanceId) {
|
|
29
27
|
throw new AuthenFailed('otp:email');
|
|
30
28
|
}
|
|
31
29
|
if (!code) {
|
|
32
30
|
throw new AuthenFailed('otp:code');
|
|
33
31
|
}
|
|
34
32
|
const otp = context.service(OTP_SERVICE);
|
|
35
|
-
await otp.verifyChallenge(email, code);
|
|
33
|
+
await otp.verifyChallenge(email, issuanceId, code);
|
|
36
34
|
const identityAlias = context.cfg.otp?.identityAlias ?? AUTH_IDENTITY_LINKING;
|
|
37
35
|
const identity = context.service(identityAlias);
|
|
38
|
-
|
|
36
|
+
// What arrives on the credential is the organization SLUG the user is logging into — it is
|
|
37
|
+
// what a person can be asked to type. The linked profile below answers with the canonical
|
|
38
|
+
// slug, which is what the token ends up carrying.
|
|
39
|
+
const entitySlug = credential.entitySlug;
|
|
39
40
|
const details = {
|
|
40
41
|
type: OTP_AUTH_TYPE,
|
|
41
42
|
service: 'email',
|
|
42
|
-
clientId:
|
|
43
|
+
clientId: entitySlug ?? 'default',
|
|
43
44
|
userId: email,
|
|
44
|
-
entityId,
|
|
45
|
+
entityId: entitySlug,
|
|
45
46
|
};
|
|
46
47
|
let payload = await identity.getLinkedProfile(details);
|
|
47
48
|
if (payload == null) {
|
|
@@ -51,7 +52,7 @@ const otpPlugin = (context) => ({
|
|
|
51
52
|
credential.type = AuthenticationType.OneTimeToken;
|
|
52
53
|
credential.userId = payload.userId;
|
|
53
54
|
credential.profileId = payload.profileId;
|
|
54
|
-
credential.
|
|
55
|
+
credential.entitySlug = payload.entitySlug;
|
|
55
56
|
credential.role = payload.role ?? AuthRole.User;
|
|
56
57
|
credential.scopes = payload.scopes ?? [ALL_SCOPES];
|
|
57
58
|
return { token: '' };
|
package/build/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAEvF,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAA;
|
|
1
|
+
{"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"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"}
|
package/build/service.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
1
|
import type { OtpService } from '@owlmeans/auth-otp';
|
|
2
|
+
export declare const otpChallengeKey: (issuanceId: string) => string;
|
|
3
|
+
export declare const otpEmailKey: (email: string) => string;
|
|
4
|
+
export declare const otpCodeHash: (issuanceId: string, code: string) => string;
|
|
2
5
|
export declare const makeOtpService: (alias?: string) => OtpService;
|
|
3
6
|
//# sourceMappingURL=service.d.ts.map
|
package/build/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAkBpD,eAAO,MAAM,eAAe,eAAgB,MAAM,KAAG,MAAuD,CAAA;AAC5G,eAAO,MAAM,WAAW,UAAW,MAAM,KAAG,MAAqD,CAAA;AACjG,eAAO,MAAM,WAAW,eAAgB,MAAM,QAAQ,MAAM,KAAG,MACd,CAAA;AAEjD,eAAO,MAAM,cAAc,sBAA0B,UAiEpD,CAAA"}
|