@restingowlorg/owlauth 1.0.1
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/LICENSE +373 -0
- package/README.md +307 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +16 -0
- package/dist/core/auth.manager.d.ts +6 -0
- package/dist/core/auth.manager.js +21 -0
- package/dist/core/auth.service.init.d.ts +2 -0
- package/dist/core/auth.service.init.js +26 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +11 -0
- package/dist/infra/databases/mongo/adapter.d.ts +6 -0
- package/dist/infra/databases/mongo/adapter.js +24 -0
- package/dist/infra/databases/mongo/db.d.ts +5 -0
- package/dist/infra/databases/mongo/db.js +43 -0
- package/dist/infra/databases/mongo/mongo.d.ts +5 -0
- package/dist/infra/databases/mongo/mongo.js +11 -0
- package/dist/infra/databases/postgresql/adapter.d.ts +6 -0
- package/dist/infra/databases/postgresql/adapter.js +26 -0
- package/dist/infra/databases/postgresql/db.d.ts +5 -0
- package/dist/infra/databases/postgresql/db.js +50 -0
- package/dist/infra/databases/postgresql/helpers.d.ts +8 -0
- package/dist/infra/databases/postgresql/helpers.js +55 -0
- package/dist/infra/databases/postgresql/postgres.d.ts +5 -0
- package/dist/infra/databases/postgresql/postgres.js +11 -0
- package/dist/infra/databases/postgresql/schema.d.ts +6 -0
- package/dist/infra/databases/postgresql/schema.js +9 -0
- package/dist/infra/security/bcrypt.adapter.d.ts +9 -0
- package/dist/infra/security/bcrypt.adapter.js +62 -0
- package/dist/infra/security/bcrypt.adapter.test.d.ts +1 -0
- package/dist/infra/security/bcrypt.adapter.test.js +67 -0
- package/dist/infra/security/pwned-passwords.d.ts +5 -0
- package/dist/infra/security/pwned-passwords.js +45 -0
- package/dist/infra/security/pwned-passwords.test.d.ts +1 -0
- package/dist/infra/security/pwned-passwords.test.js +62 -0
- package/dist/infra/security/security-audit-logger.d.ts +11 -0
- package/dist/infra/security/security-audit-logger.js +90 -0
- package/dist/repositories/contracts.d.ts +21 -0
- package/dist/repositories/contracts.js +2 -0
- package/dist/repositories/mongo/magicLink.repo.d.ts +26 -0
- package/dist/repositories/mongo/magicLink.repo.js +106 -0
- package/dist/repositories/mongo/user.repo.d.ts +16 -0
- package/dist/repositories/mongo/user.repo.js +84 -0
- package/dist/repositories/postgresql/magic.link.repo.d.ts +21 -0
- package/dist/repositories/postgresql/magic.link.repo.js +97 -0
- package/dist/repositories/postgresql/user.repo.d.ts +14 -0
- package/dist/repositories/postgresql/user.repo.js +50 -0
- package/dist/services/auth.service.d.ts +22 -0
- package/dist/services/auth.service.js +362 -0
- package/dist/services/auth.service.test.d.ts +1 -0
- package/dist/services/auth.service.test.js +297 -0
- package/dist/services/magic-link.service.d.ts +22 -0
- package/dist/services/magic-link.service.js +196 -0
- package/dist/services/magic-link.service.test.d.ts +1 -0
- package/dist/services/magic-link.service.test.js +230 -0
- package/dist/strategies/CredentialsStrategy.d.ts +4 -0
- package/dist/strategies/CredentialsStrategy.js +32 -0
- package/dist/strategies/CredentialsStrategy.test.d.ts +1 -0
- package/dist/strategies/CredentialsStrategy.test.js +29 -0
- package/dist/strategies/MagicLinkStrategy.d.ts +4 -0
- package/dist/strategies/MagicLinkStrategy.js +21 -0
- package/dist/strategies/MagicLinkStrategy.test.d.ts +1 -0
- package/dist/strategies/MagicLinkStrategy.test.js +38 -0
- package/dist/types/index.d.ts +224 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/check-blocked-passwords.d.ts +1 -0
- package/dist/utils/check-blocked-passwords.js +10 -0
- package/dist/utils/check-blocked-passwords.test.d.ts +1 -0
- package/dist/utils/check-blocked-passwords.test.js +27 -0
- package/package.json +102 -0
package/README.md
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# owlauth
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://restingowl.com/owl-logo.png" alt="owlauth logo" width="120" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@restingowlorg/owlauth) [](https://www.npmjs.com/package/@restingowlorg/owlauth) [](https://www.npmjs.com/package/@restingowlorg/owlauth) [](LICENSE)
|
|
10
|
+
|
|
11
|
+
Open-source OWASP-aligned authentication and account-security library for Node.js.
|
|
12
|
+
|
|
13
|
+
owlauth, published as `@restingowlorg/owlauth`, gives your Node.js app the core pieces of authentication: credentials login, magic links, password checks, and security-focused audit logging. It works with PostgreSQL and MongoDB and stays out of your framework.
|
|
14
|
+
|
|
15
|
+
- **Package:** `@restingowlorg/owlauth`
|
|
16
|
+
- **Latest stable tag:** `latest`
|
|
17
|
+
- **Prerelease tag:** `next`
|
|
18
|
+
- **Install:** `npm install @restingowlorg/owlauth`
|
|
19
|
+
- **Developer guide:** [docs/DEVELOPER_GUIDE.md](docs/DEVELOPER_GUIDE.md)
|
|
20
|
+
|
|
21
|
+
## What You Get
|
|
22
|
+
|
|
23
|
+
- **Credentials authentication**: Sign up users, log them in, and rotate passwords through one library surface.
|
|
24
|
+
- **Passwordless magic links**: Request, verify, and consume single-use login tokens.
|
|
25
|
+
- **PostgreSQL and MongoDB adapters**: Use the same auth API across two common persistence stacks.
|
|
26
|
+
- **Password hygiene controls**: Enforce minimum strength with `zxcvbn`, reject context-based weak passwords, and check candidate passwords against Have I Been Pwned using the k-anonymity API.
|
|
27
|
+
- **Security-focused logging**: Audit events are logged with built-in masking for sensitive fields such as passwords, tokens, secrets, cookies, and authorization data.
|
|
28
|
+
- **Request tracing support**: Pass a `correlationId` through auth operations to align library logs with your application logs.
|
|
29
|
+
- **Pluggable cryptography**: Swap the default crypto adapter if your stack requires a different hashing or token strategy.
|
|
30
|
+
- **Typed, predictable results**: Every auth method returns a consistent `AuthResult<T>` shape.
|
|
31
|
+
|
|
32
|
+
## Support Matrix
|
|
33
|
+
|
|
34
|
+
| Area | Current Support |
|
|
35
|
+
| ------------- | ----------------------- |
|
|
36
|
+
| Runtime | Node.js 18+ |
|
|
37
|
+
| Language | TypeScript, JavaScript |
|
|
38
|
+
| Module output | CommonJS |
|
|
39
|
+
| Databases | PostgreSQL, MongoDB |
|
|
40
|
+
| Auth flows | Credentials, Magic Link |
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @restingowlorg/owlauth
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { createAuthManager, PostgresAdapter } from "@restingowlorg/owlauth";
|
|
52
|
+
|
|
53
|
+
const auth = await createAuthManager({
|
|
54
|
+
adapter: new PostgresAdapter({
|
|
55
|
+
postgresUrl: process.env.POSTGRES_URL!,
|
|
56
|
+
userTableName: "users",
|
|
57
|
+
magicLinkTableName: "magic_links"
|
|
58
|
+
}),
|
|
59
|
+
authTypes: ["credentials", "magicLink"],
|
|
60
|
+
blockedPasswords: ["company-name", "product-name"],
|
|
61
|
+
pwnedPasswordFailClosed: true,
|
|
62
|
+
customMaskingKeys: ["apiKey", "accessToken"]
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const signup = await auth.credentials.signup(
|
|
66
|
+
"user@example.com",
|
|
67
|
+
"engineer01",
|
|
68
|
+
"CorrectHorseBatteryStaple!2026",
|
|
69
|
+
{ correlationId: "req_123" }
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (signup.success) {
|
|
73
|
+
console.log(signup.data.user.email);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await auth.disconnectDB();
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Database Adapters
|
|
80
|
+
|
|
81
|
+
### PostgreSQL
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { createAuthManager, PostgresAdapter } from "@restingowlorg/owlauth";
|
|
85
|
+
|
|
86
|
+
const auth = await createAuthManager({
|
|
87
|
+
adapter: new PostgresAdapter({
|
|
88
|
+
postgresUrl: process.env.POSTGRES_URL!,
|
|
89
|
+
userTableName: "users",
|
|
90
|
+
userSchema: "public",
|
|
91
|
+
magicLinkTableName: "magic_links",
|
|
92
|
+
magicLinkSchema: "public"
|
|
93
|
+
}),
|
|
94
|
+
authTypes: ["credentials", "magicLink"]
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### MongoDB
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { createAuthManager, MongoAdapter } from "@restingowlorg/owlauth";
|
|
102
|
+
|
|
103
|
+
const auth = await createAuthManager({
|
|
104
|
+
adapter: new MongoAdapter({
|
|
105
|
+
mongoUri: process.env.MONGO_URI!,
|
|
106
|
+
userCollectionName: "users",
|
|
107
|
+
magicLinkCollectionName: "magic_links"
|
|
108
|
+
}),
|
|
109
|
+
authTypes: ["credentials", "magicLink"]
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Subpath Exports
|
|
114
|
+
|
|
115
|
+
Need just the adapter layer? The package ships database-specific entry points too:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import {
|
|
119
|
+
MongoAdapter,
|
|
120
|
+
MongoUserRepo,
|
|
121
|
+
MongoMagicLinkRepo,
|
|
122
|
+
connectMongo
|
|
123
|
+
} from "@restingowlorg/owlauth/mongo";
|
|
124
|
+
import {
|
|
125
|
+
PostgresAdapter,
|
|
126
|
+
PostgresUserRepository,
|
|
127
|
+
PostgresMagicLinkRepository,
|
|
128
|
+
initPostgres
|
|
129
|
+
} from "@restingowlorg/owlauth/postgres";
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Core Usage
|
|
133
|
+
|
|
134
|
+
### Credentials Flow
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import {
|
|
138
|
+
AuthResult,
|
|
139
|
+
SignupResult,
|
|
140
|
+
LoginResult,
|
|
141
|
+
ChangePasswordResult
|
|
142
|
+
} from "@restingowlorg/owlauth";
|
|
143
|
+
|
|
144
|
+
const signup: AuthResult<SignupResult> = await auth.credentials.signup(
|
|
145
|
+
"user@example.com",
|
|
146
|
+
"engineer01",
|
|
147
|
+
"CorrectHorseBatteryStaple!2026"
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const login: AuthResult<LoginResult> = await auth.credentials.login(
|
|
151
|
+
"user@example.com",
|
|
152
|
+
"CorrectHorseBatteryStaple!2026",
|
|
153
|
+
{
|
|
154
|
+
correlationId: "req_login_001"
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const passwordChange: AuthResult<ChangePasswordResult> = await auth.credentials.changePassword(
|
|
159
|
+
"user_id123456",
|
|
160
|
+
"current_strong_password",
|
|
161
|
+
"new_strong_password_example",
|
|
162
|
+
{
|
|
163
|
+
correlationId: "req_password_001"
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Magic Link Flow
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import {
|
|
172
|
+
AuthResult,
|
|
173
|
+
RequestMagicLinkResult,
|
|
174
|
+
VerifyMagicLinkResult,
|
|
175
|
+
ConsumeMagicLinkResult
|
|
176
|
+
} from "@restingowlorg/owlauth";
|
|
177
|
+
|
|
178
|
+
const requested: AuthResult<RequestMagicLinkResult> = await auth.magicLink.request(
|
|
179
|
+
"user@example.com",
|
|
180
|
+
{ correlationId: "req_magic_001" }
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (requested.success) {
|
|
184
|
+
const token = requested.data;
|
|
185
|
+
const verified: AuthResult<VerifyMagicLinkResult> = await auth.magicLink.verify(token);
|
|
186
|
+
const consumed: AuthResult<ConsumeMagicLinkResult> = await auth.magicLink.consume(token);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`request()` returns the raw token string. Putting it in a URL, sending the email, and handling delivery is your application's job. owlauth does not touch any of that.
|
|
191
|
+
|
|
192
|
+
## Configuration Options
|
|
193
|
+
|
|
194
|
+
### Shared Options
|
|
195
|
+
|
|
196
|
+
| Option | Type | Purpose |
|
|
197
|
+
| ------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------- |
|
|
198
|
+
| `authTypes` | `("credentials" \| "magicLink")[]` | Enables one or both supported auth flows. Defaults to credentials only. |
|
|
199
|
+
| `blockedPasswords` | `string[]` | Rejects passwords containing the user's email, username, or any supplied blocked terms. |
|
|
200
|
+
| `cryptoAdapter` | `ICryptoAdapter` | Replaces the default bcrypt-based crypto implementation. |
|
|
201
|
+
| `customMaskingKeys` | `string[]` | Adds case-insensitive keys to the audit logger masking list. |
|
|
202
|
+
| `pwnedPasswordFailClosed` | `boolean` | Rejects signups and password changes when the breached-password API cannot be reached. |
|
|
203
|
+
|
|
204
|
+
### Method-Level Options
|
|
205
|
+
|
|
206
|
+
- `signup()`: `blockedPasswords`, `pwnedPasswordFailClosed`, `correlationId`
|
|
207
|
+
- `login()`: `correlationId`
|
|
208
|
+
- `changePassword()`: `blockedPasswords`, `pwnedPasswordFailClosed`, `correlationId`
|
|
209
|
+
- `magicLink.request()`: `correlationId`
|
|
210
|
+
- `magicLink.verify()`: `correlationId`
|
|
211
|
+
- `magicLink.consume()`: `correlationId`
|
|
212
|
+
|
|
213
|
+
## Response Model
|
|
214
|
+
|
|
215
|
+
Every public method returns the same envelope:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
type AuthResult<T = unknown> =
|
|
219
|
+
| { success: true; data: T; httpCode: number; message: string }
|
|
220
|
+
| { success: false; data?: undefined; httpCode: number; message: string };
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Result payloads are:
|
|
224
|
+
|
|
225
|
+
- `SignupResult`: `{ user: { id, email, username } }`
|
|
226
|
+
- `LoginResult`: `{ user: { id, email, username } }`
|
|
227
|
+
- `ChangePasswordResult`: `{ user: { id, email, username } }`
|
|
228
|
+
- `RequestMagicLinkResult`: `string`
|
|
229
|
+
- `VerifyMagicLinkResult`: `{ isValid: true, userId: string, tokenId: string }`
|
|
230
|
+
- `ConsumeMagicLinkResult`: `{ userId: string }`
|
|
231
|
+
|
|
232
|
+
## OWASP Alignment
|
|
233
|
+
|
|
234
|
+
Here's exactly what owlauth does, and where each decision comes from. Every control is traced back to the [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html), [ASVS 5.0.0](https://owasp.org/www-project-application-security-verification-standard/), or [OWASP Top 10:2025](https://owasp.org/www-project-top-ten/).
|
|
235
|
+
|
|
236
|
+
### Password Security
|
|
237
|
+
|
|
238
|
+
| Control | What the library does | OWASP reference |
|
|
239
|
+
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
240
|
+
| Password strength scoring | Candidate passwords are scored with `@zxcvbn-ts/core` at signup and on every password change. Scores below 3 of 4 are rejected. | [Auth Cheat Sheet — Implement Proper Password Strength Controls](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#implement-proper-password-strength-controls) |
|
|
241
|
+
| Breached password check | The [Have I Been Pwned Pwned Passwords API](https://haveibeenpwned.com/API/v3#PwnedPasswords) is queried using the k-anonymity range method. Only the first 5 characters of the SHA-1 hash are transmitted; the raw password never leaves the process. | [Auth Cheat Sheet — Block previously breached passwords](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#implement-proper-password-strength-controls) |
|
|
242
|
+
| Context-aware blocking | Passwords are rejected if they contain the user's email local part, username, or any caller-supplied blocked terms, preventing context-guessable passwords. | [NIST SP 800-63B § 5.1.1.2](https://pages.nist.gov/800-63-4/sp800-63b.html), context-specific word verification |
|
|
243
|
+
| Fail-closed breach check | When `pwnedPasswordFailClosed: true`, a network error on the breach API causes the request to fail rather than silently pass. | Defense-in-depth, fail-safe defaults |
|
|
244
|
+
|
|
245
|
+
### Credential Storage
|
|
246
|
+
|
|
247
|
+
| Control | What the library does | OWASP reference |
|
|
248
|
+
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
249
|
+
| Adaptive password hashing | Passwords are hashed with bcrypt at 10 salt rounds via the built-in `BcryptAdapter`. | [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html), use bcrypt |
|
|
250
|
+
| Pluggable crypto adapter | The `ICryptoAdapter` interface allows swapping the default bcrypt implementation for any alternative hashing or token strategy without changing the auth API surface. | ASVS 5.0, algorithm agility |
|
|
251
|
+
|
|
252
|
+
### Authentication Logic
|
|
253
|
+
|
|
254
|
+
| Control | What the library does | OWASP reference |
|
|
255
|
+
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
256
|
+
| Generic login error messages | Login failure returns `"Invalid credentials."` regardless of whether the email is unknown or the password is wrong, preventing user enumeration. | [Auth Cheat Sheet, Authentication and Error Messages](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-and-error-messages) |
|
|
257
|
+
| Current password re-verification | `changePassword()` requires the caller to supply and verify the current password before a new one is accepted, preventing silent takeover through a hijacked session. | [Auth Cheat Sheet, Change Password Feature](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#change-password-feature) |
|
|
258
|
+
|
|
259
|
+
### Passwordless Authentication
|
|
260
|
+
|
|
261
|
+
| Control | What the library does | OWASP reference |
|
|
262
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
263
|
+
| Cryptographically secure token generation | Magic link tokens are produced with `crypto.randomBytes(32)` from Node.js's built-in CSPRNG. | [OWASP Forgot Password Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html), use a cryptographically random value |
|
|
264
|
+
| Token hashing at rest | The raw token is never stored. Only the bcrypt hash of the token is persisted; the database contains no recoverable plaintext. | [OWASP Forgot Password Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html), hash tokens before storage |
|
|
265
|
+
| Short expiry window | Tokens expire 15 minutes after issuance. | [OWASP Forgot Password Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html), use a short token lifetime |
|
|
266
|
+
| Single-use with prior invalidation | Requesting a new magic link immediately invalidates all previous active tokens for that user. Consumed tokens cannot be reused. | [OWASP Forgot Password Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Forgot_Password_Cheat_Sheet.html), single-use tokens |
|
|
267
|
+
|
|
268
|
+
### Security Logging and Audit Trail
|
|
269
|
+
|
|
270
|
+
| Control | What the library does | OWASP reference |
|
|
271
|
+
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
272
|
+
| Security event logging | Every auth operation, signup, login, password change, and all magic link steps, emits a structured audit event with event type, email, outcome, and reason. | [Auth Cheat Sheet, Logging and Monitoring](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#logging-and-monitoring); [A09:2025 Security Logging and Alerting Failures](https://owasp.org/www-project-top-ten/) |
|
|
273
|
+
| Sensitive field masking | The audit logger automatically redacts values at keys matching `password`, `token`, `secret`, `authorization`, `cookie`, and `apikey`. Callers can extend this with `customMaskingKeys`. | ASVS 5.0, do not log sensitive data; [OWASP Logging Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html) |
|
|
274
|
+
| Correlation ID propagation | Every auth method accepts an optional `correlationId`. When supplied, it is included in all log output for that operation, enabling trace correlation with application-level logs. | [OWASP Logging Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html), include trace identifiers |
|
|
275
|
+
|
|
276
|
+
## Security Notes
|
|
277
|
+
|
|
278
|
+
The table above covers what this library actually does. It's **not** an OWASP certification, and it won't make your app ASVS-compliant on its own. You still need to handle:
|
|
279
|
+
|
|
280
|
+
- TLS and secure transport
|
|
281
|
+
- secure email delivery for magic links
|
|
282
|
+
- rate limiting, brute-force protection, and account lockout
|
|
283
|
+
- session management
|
|
284
|
+
- CSRF defenses where relevant
|
|
285
|
+
- account verification and recovery workflows
|
|
286
|
+
- MFA or passkeys if the risk model requires them
|
|
287
|
+
- authorization and role enforcement
|
|
288
|
+
|
|
289
|
+
## Roadmap
|
|
290
|
+
|
|
291
|
+
owlauth is part of a wider RestingOwl effort focused on building secure-by-default tooling for various technology stacks, not limited to Node.js.
|
|
292
|
+
|
|
293
|
+
Here's what's coming next:
|
|
294
|
+
|
|
295
|
+
- **More application stacks**: First-party integrations for Express, Fastify, NestJS, Next.js, and serverless Node runtimes
|
|
296
|
+
- **More data stores**: Additional adapters for MySQL, SQLite, DynamoDB, and other operationally common backends
|
|
297
|
+
- **Stronger auth options**: WebAuthn, passkeys, TOTP-based MFA, and recovery-oriented flows
|
|
298
|
+
- **Operational hardening**: Built-in rate limiting hooks, lockout strategies, and safer recovery patterns
|
|
299
|
+
- **Broader RestingOwl package family**: Adjacent packages for rate limiting, input sanitization, audit logging, secrets management, and CSRF protection
|
|
300
|
+
|
|
301
|
+
## Community
|
|
302
|
+
|
|
303
|
+
[](https://restingowl.com/) [](https://www.linkedin.com/showcase/restingowl/) [](https://github.com/restingowlorg/OwlAuth) [](https://github.com/restingowlorg/OwlAuth/issues) [](SECURITY.md) [](CONTRIBUTING.md)
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
[Mozilla Public License 2.0](LICENSE)
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare const SECURITY_CONFIG: {
|
|
2
|
+
readonly LOGGER_PREFIX: "OWL-AUTH";
|
|
3
|
+
readonly SENSITIVE_KEYS: readonly ["password", "token", "secret", "authorization", "cookie", "apikey"];
|
|
4
|
+
/**
|
|
5
|
+
* Have I Been Pwned API URL for range-based hash checking
|
|
6
|
+
* @see https://haveibeenpwned.com/API/v3#PwnedPasswords
|
|
7
|
+
*/
|
|
8
|
+
readonly PWNED_API_URL: "https://api.pwnedpasswords.com/range";
|
|
9
|
+
/**
|
|
10
|
+
* Bcrypt salt rounds
|
|
11
|
+
*/
|
|
12
|
+
readonly SALT_ROUNDS: 10;
|
|
13
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SECURITY_CONFIG = void 0;
|
|
4
|
+
exports.SECURITY_CONFIG = {
|
|
5
|
+
LOGGER_PREFIX: "OWL-AUTH",
|
|
6
|
+
SENSITIVE_KEYS: ["password", "token", "secret", "authorization", "cookie", "apikey"],
|
|
7
|
+
/**
|
|
8
|
+
* Have I Been Pwned API URL for range-based hash checking
|
|
9
|
+
* @see https://haveibeenpwned.com/API/v3#PwnedPasswords
|
|
10
|
+
*/
|
|
11
|
+
PWNED_API_URL: "https://api.pwnedpasswords.com/range",
|
|
12
|
+
/**
|
|
13
|
+
* Bcrypt salt rounds
|
|
14
|
+
*/
|
|
15
|
+
SALT_ROUNDS: 10
|
|
16
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AuthOptions, IAuthManager, AuthType } from "../types/index";
|
|
2
|
+
/**
|
|
3
|
+
* Creates and initializes an instance of the AuthManager.
|
|
4
|
+
* Returns the IAuthManager interface to consumers.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createAuthManager<T extends AuthType = "credentials">(options: AuthOptions<T>): Promise<IAuthManager<T>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createAuthManager = createAuthManager;
|
|
4
|
+
const auth_service_init_1 = require("./auth.service.init");
|
|
5
|
+
/**
|
|
6
|
+
* Creates and initializes an instance of the AuthManager.
|
|
7
|
+
* Returns the IAuthManager interface to consumers.
|
|
8
|
+
*/
|
|
9
|
+
async function createAuthManager(options) {
|
|
10
|
+
// Database
|
|
11
|
+
if (!options.adapter) {
|
|
12
|
+
throw new Error("[Auth:createAuthManager] Database adapter is required in AuthOptions");
|
|
13
|
+
}
|
|
14
|
+
const db = await options.adapter.connect(options);
|
|
15
|
+
// Auth Services
|
|
16
|
+
const services = (0, auth_service_init_1.initAuthServices)(db, options);
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
...services,
|
|
19
|
+
disconnectDB: db.close
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initAuthServices = initAuthServices;
|
|
4
|
+
const security_audit_logger_1 = require("../infra/security/security-audit-logger");
|
|
5
|
+
const CredentialsStrategy_1 = require("../strategies/CredentialsStrategy");
|
|
6
|
+
const MagicLinkStrategy_1 = require("../strategies/MagicLinkStrategy");
|
|
7
|
+
const authStrategies = {
|
|
8
|
+
credentials: new CredentialsStrategy_1.CredentialsAuthStrategy(),
|
|
9
|
+
magicLink: new MagicLinkStrategy_1.MagicLinkAuthStrategy()
|
|
10
|
+
};
|
|
11
|
+
function initAuthServices(db, options) {
|
|
12
|
+
var _a;
|
|
13
|
+
const result = {};
|
|
14
|
+
const authTypes = (_a = options.authTypes) !== null && _a !== void 0 ? _a : ["credentials"];
|
|
15
|
+
if (options.customMaskingKeys) {
|
|
16
|
+
security_audit_logger_1.auditLogger.setCustomMaskingKeys(options.customMaskingKeys);
|
|
17
|
+
}
|
|
18
|
+
security_audit_logger_1.auditLogger.info(`Initializing auth services for types: ${authTypes.join(", ")}`);
|
|
19
|
+
for (const type of authTypes) {
|
|
20
|
+
const strategy = authStrategies[type];
|
|
21
|
+
if (strategy) {
|
|
22
|
+
strategy.register(result, db, options);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createAuthManager } from "./core/auth.manager";
|
|
2
|
+
export { AuthType, AuthLogLevel, UserId, IAuthManager, IDatabaseAdapter, ICryptoAdapter, AuthDB, AuthOptions, BaseAuthOptions, InitPostgresOptions, InitMongoOptions, SignupInput, LoginInput, CreateMagicLinkInput, CreateUserInput, AuthResult, LoginResult, SignupResult, ChangePasswordResult, RequestMagicLinkResult, VerifyMagicLinkResult, ConsumeMagicLinkResult } from "./types/index";
|
|
3
|
+
export { BcryptAdapter } from "./infra/security/bcrypt.adapter";
|
|
4
|
+
export { MongoAdapter } from "./infra/databases/mongo/adapter";
|
|
5
|
+
export { PostgresAdapter } from "./infra/databases/postgresql/adapter";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostgresAdapter = exports.MongoAdapter = exports.BcryptAdapter = exports.createAuthManager = void 0;
|
|
4
|
+
var auth_manager_1 = require("./core/auth.manager");
|
|
5
|
+
Object.defineProperty(exports, "createAuthManager", { enumerable: true, get: function () { return auth_manager_1.createAuthManager; } });
|
|
6
|
+
var bcrypt_adapter_1 = require("./infra/security/bcrypt.adapter");
|
|
7
|
+
Object.defineProperty(exports, "BcryptAdapter", { enumerable: true, get: function () { return bcrypt_adapter_1.BcryptAdapter; } });
|
|
8
|
+
var adapter_1 = require("./infra/databases/mongo/adapter");
|
|
9
|
+
Object.defineProperty(exports, "MongoAdapter", { enumerable: true, get: function () { return adapter_1.MongoAdapter; } });
|
|
10
|
+
var adapter_2 = require("./infra/databases/postgresql/adapter");
|
|
11
|
+
Object.defineProperty(exports, "PostgresAdapter", { enumerable: true, get: function () { return adapter_2.PostgresAdapter; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AuthDB, BaseAuthOptions, IDatabaseAdapter, InitMongoOptions } from "../../../types/index";
|
|
2
|
+
export declare class MongoAdapter implements IDatabaseAdapter {
|
|
3
|
+
private readonly config;
|
|
4
|
+
constructor(config: InitMongoOptions);
|
|
5
|
+
connect(options: BaseAuthOptions): Promise<AuthDB>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MongoAdapter = void 0;
|
|
4
|
+
const db_1 = require("./db");
|
|
5
|
+
class MongoAdapter {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
async connect(options) {
|
|
10
|
+
const { mongoUri, userCollectionName, magicLinkCollectionName } = this.config;
|
|
11
|
+
const { authTypes } = options;
|
|
12
|
+
if (!mongoUri)
|
|
13
|
+
throw new Error("[Auth:MongoAdapter] mongoUri is required for MongoAdapter");
|
|
14
|
+
if (!userCollectionName)
|
|
15
|
+
throw new Error("[Auth:MongoAdapter] userCollectionName is required for MongoAdapter");
|
|
16
|
+
return await (0, db_1.connectMongo)({
|
|
17
|
+
mongoUri,
|
|
18
|
+
userCollectionName,
|
|
19
|
+
magicLinkCollectionName,
|
|
20
|
+
authTypes
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.MongoAdapter = MongoAdapter;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.connectMongo = connectMongo;
|
|
4
|
+
const mongodb_1 = require("mongodb");
|
|
5
|
+
const magicLink_repo_1 = require("../../../repositories/mongo/magicLink.repo");
|
|
6
|
+
const user_repo_1 = require("../../../repositories/mongo/user.repo");
|
|
7
|
+
const security_audit_logger_1 = require("../../security/security-audit-logger");
|
|
8
|
+
/**
|
|
9
|
+
* Connect to MongoDB and initialize repositories
|
|
10
|
+
*/
|
|
11
|
+
async function connectMongo(options) {
|
|
12
|
+
const { mongoUri, userCollectionName, magicLinkCollectionName, authTypes } = options;
|
|
13
|
+
if (!mongoUri)
|
|
14
|
+
throw new Error("[Auth:connectMongo] mongoUri is required");
|
|
15
|
+
if (!userCollectionName)
|
|
16
|
+
throw new Error("[Auth:connectMongo] userCollectionName is required");
|
|
17
|
+
// Connect to MongoDB
|
|
18
|
+
const client = new mongodb_1.MongoClient(mongoUri);
|
|
19
|
+
await client.connect();
|
|
20
|
+
const db = client.db();
|
|
21
|
+
if (!db) {
|
|
22
|
+
security_audit_logger_1.auditLogger.error("Failed to connect to MongoDB - no database instance found", new Error("No DB instance"));
|
|
23
|
+
throw new Error("[Auth:connectMongo] Failed to connect to MongoDB");
|
|
24
|
+
}
|
|
25
|
+
// Use generic to type collection correctly
|
|
26
|
+
const userColl = db.collection(userCollectionName);
|
|
27
|
+
// Magic link collection
|
|
28
|
+
let magicColl;
|
|
29
|
+
if (authTypes === null || authTypes === void 0 ? void 0 : authTypes.includes("magicLink")) {
|
|
30
|
+
if (!magicLinkCollectionName) {
|
|
31
|
+
throw new Error(`[Auth:connectMongo] Magic link auth requested but 'magicLinkCollectionName' is not provided`);
|
|
32
|
+
}
|
|
33
|
+
magicColl = db.collection(magicLinkCollectionName);
|
|
34
|
+
}
|
|
35
|
+
// Initialize repositories
|
|
36
|
+
return {
|
|
37
|
+
userRepo: new user_repo_1.MongoUserRepo(userColl),
|
|
38
|
+
magicLinkRepo: magicColl ? new magicLink_repo_1.MongoMagicLinkRepo(magicColl) : undefined,
|
|
39
|
+
close: async () => {
|
|
40
|
+
await client.close();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { MongoAdapter } from "./adapter";
|
|
2
|
+
export { connectMongo } from "./db";
|
|
3
|
+
export { MongoUserRepo } from "../../../repositories/mongo/user.repo";
|
|
4
|
+
export { MongoMagicLinkRepo } from "../../../repositories/mongo/magicLink.repo";
|
|
5
|
+
export { IMongoUserDoc, IMongoMagicLinkDoc } from "../../../types/index";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MongoMagicLinkRepo = exports.MongoUserRepo = exports.connectMongo = exports.MongoAdapter = void 0;
|
|
4
|
+
var adapter_1 = require("./adapter");
|
|
5
|
+
Object.defineProperty(exports, "MongoAdapter", { enumerable: true, get: function () { return adapter_1.MongoAdapter; } });
|
|
6
|
+
var db_1 = require("./db");
|
|
7
|
+
Object.defineProperty(exports, "connectMongo", { enumerable: true, get: function () { return db_1.connectMongo; } });
|
|
8
|
+
var user_repo_1 = require("../../../repositories/mongo/user.repo");
|
|
9
|
+
Object.defineProperty(exports, "MongoUserRepo", { enumerable: true, get: function () { return user_repo_1.MongoUserRepo; } });
|
|
10
|
+
var magicLink_repo_1 = require("../../../repositories/mongo/magicLink.repo");
|
|
11
|
+
Object.defineProperty(exports, "MongoMagicLinkRepo", { enumerable: true, get: function () { return magicLink_repo_1.MongoMagicLinkRepo; } });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AuthDB, BaseAuthOptions, IDatabaseAdapter, InitPostgresOptions } from "../../../types/index";
|
|
2
|
+
export declare class PostgresAdapter implements IDatabaseAdapter {
|
|
3
|
+
private readonly config;
|
|
4
|
+
constructor(config: InitPostgresOptions);
|
|
5
|
+
connect(options: BaseAuthOptions): Promise<AuthDB>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PostgresAdapter = void 0;
|
|
4
|
+
const db_1 = require("./db");
|
|
5
|
+
class PostgresAdapter {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
async connect(options) {
|
|
10
|
+
const { postgresUrl, userTableName, userSchema, magicLinkTableName, magicLinkSchema } = this.config;
|
|
11
|
+
const { authTypes } = options;
|
|
12
|
+
if (!postgresUrl)
|
|
13
|
+
throw new Error("[Auth:PostgresAdapter] postgresUrl is required for PostgresAdapter");
|
|
14
|
+
if (!userTableName)
|
|
15
|
+
throw new Error("[Auth:PostgresAdapter] userTableName is required for PostgresAdapter");
|
|
16
|
+
return await (0, db_1.initPostgres)({
|
|
17
|
+
postgresUrl,
|
|
18
|
+
userTableName,
|
|
19
|
+
userSchema,
|
|
20
|
+
magicLinkTableName,
|
|
21
|
+
magicLinkSchema,
|
|
22
|
+
authTypes
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.PostgresAdapter = PostgresAdapter;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.initPostgres = initPostgres;
|
|
4
|
+
const pg_1 = require("pg");
|
|
5
|
+
const user_repo_1 = require("../../../repositories/postgresql/user.repo");
|
|
6
|
+
const magic_link_repo_1 = require("../../../repositories/postgresql/magic.link.repo");
|
|
7
|
+
const schema_1 = require("./schema");
|
|
8
|
+
const helpers_1 = require("./helpers");
|
|
9
|
+
/**
|
|
10
|
+
* Initialize PostgreSQL connection and repositories
|
|
11
|
+
*/
|
|
12
|
+
async function initPostgres(options) {
|
|
13
|
+
const { postgresUrl, userTableName, userSchema = "public", magicLinkTableName, magicLinkSchema = "public", authTypes } = options;
|
|
14
|
+
if (!postgresUrl)
|
|
15
|
+
throw new Error("[Auth:initPostgres] postgresUrl is required");
|
|
16
|
+
if (!userTableName)
|
|
17
|
+
throw new Error("[Auth:initPostgres] userTableName is required");
|
|
18
|
+
const pool = new pg_1.Pool({ connectionString: postgresUrl });
|
|
19
|
+
const isConnected = await pool.query("SELECT 1"); // Test connection
|
|
20
|
+
if (!isConnected)
|
|
21
|
+
throw new Error("[Auth:initPostgres] Failed to connect to PostgreSQL");
|
|
22
|
+
const qualifiedUserTable = `${userSchema}.${userTableName}`;
|
|
23
|
+
// Core User table validations
|
|
24
|
+
await Promise.all([
|
|
25
|
+
(0, helpers_1.validateSchema)(pool, userSchema),
|
|
26
|
+
(0, helpers_1.validateTable)(pool, qualifiedUserTable),
|
|
27
|
+
(0, helpers_1.validateColumns)(pool, userSchema, userTableName, schema_1.PostgresUserSchema.requiredColumns)
|
|
28
|
+
]);
|
|
29
|
+
// Magic link table validations (if enabled)
|
|
30
|
+
let magicRepo;
|
|
31
|
+
if (authTypes === null || authTypes === void 0 ? void 0 : authTypes.includes("magicLink")) {
|
|
32
|
+
const magicTable = magicLinkTableName !== null && magicLinkTableName !== void 0 ? magicLinkTableName : "magic_links";
|
|
33
|
+
const qualifiedMagicTable = `${magicLinkSchema}.${magicTable}`;
|
|
34
|
+
await Promise.all([
|
|
35
|
+
(0, helpers_1.validateSchema)(pool, magicLinkSchema),
|
|
36
|
+
(0, helpers_1.validateTable)(pool, qualifiedMagicTable),
|
|
37
|
+
(0, helpers_1.validateColumns)(pool, magicLinkSchema, magicTable, schema_1.PostgresMagicLinkSchema.requiredColumns),
|
|
38
|
+
(0, helpers_1.validateForeignKey)(pool, magicLinkSchema, magicTable, userSchema, userTableName, "user_id", "id")
|
|
39
|
+
]);
|
|
40
|
+
magicRepo = new magic_link_repo_1.PostgresMagicLinkRepository(qualifiedMagicTable, pool);
|
|
41
|
+
}
|
|
42
|
+
// Return repositories
|
|
43
|
+
return {
|
|
44
|
+
userRepo: new user_repo_1.PostgresUserRepository(qualifiedUserTable, pool),
|
|
45
|
+
magicLinkRepo: magicRepo,
|
|
46
|
+
close: async () => {
|
|
47
|
+
await pool.end();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Pool } from "pg";
|
|
2
|
+
/**
|
|
3
|
+
* Validation Helpers for PostgreSQL
|
|
4
|
+
*/
|
|
5
|
+
export declare function validateSchema(pool: Pool, schema: string): Promise<void>;
|
|
6
|
+
export declare function validateTable(pool: Pool, qualifiedTable: string): Promise<void>;
|
|
7
|
+
export declare function validateColumns(pool: Pool, schema: string, table: string, requiredColumns: readonly string[]): Promise<void>;
|
|
8
|
+
export declare function validateForeignKey(pool: Pool, schema: string, table: string, refSchema: string, refTable: string, col: string, refCol: string): Promise<void>;
|