@ttoss/http-server-auth 0.2.0
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 +21 -0
- package/README.md +121 -0
- package/dist/index.cjs +135 -0
- package/dist/index.d.cts +72 -0
- package/dist/index.d.mts +72 -0
- package/dist/index.mjs +102 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Terezinha Tech Operations (ttoss)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @ttoss/http-server-auth
|
|
2
|
+
|
|
3
|
+
Authentication middleware for `@ttoss/http-server` (Koa). Wraps `@ttoss/auth-core` primitives into a ready-to-use Bearer-token strategy chain.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @ttoss/http-server-auth
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Global middleware
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { authMiddleware } from '@ttoss/http-server-auth';
|
|
17
|
+
import { App } from '@ttoss/http-server';
|
|
18
|
+
|
|
19
|
+
const app = new App();
|
|
20
|
+
|
|
21
|
+
app.use(
|
|
22
|
+
authMiddleware({
|
|
23
|
+
strategies: ['jwt', 'apiToken', 'system'],
|
|
24
|
+
|
|
25
|
+
jwt: {
|
|
26
|
+
secret: process.env.JWT_SECRET,
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
apiToken: {
|
|
30
|
+
lookup: async (tokenHash) => {
|
|
31
|
+
const record = await ApiTokenModel.findOne({
|
|
32
|
+
where: { tokenHash, revoked: false },
|
|
33
|
+
});
|
|
34
|
+
if (!record) return null;
|
|
35
|
+
await record.update({ lastUsedAt: new Date() });
|
|
36
|
+
return { id: record.user.publicId, email: record.user.email };
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
system: {
|
|
41
|
+
secret: process.env.INTERNAL_API_SECRET,
|
|
42
|
+
user: { id: 'system', email: 'system@internal' },
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
allowedOrigins: [
|
|
46
|
+
process.env.APP_URL,
|
|
47
|
+
'http://localhost:3000',
|
|
48
|
+
/\.vercel\.app$/,
|
|
49
|
+
],
|
|
50
|
+
|
|
51
|
+
required: true, // default
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Per-route middleware
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { requireAuth } from '@ttoss/http-server-auth';
|
|
60
|
+
import { Router } from '@ttoss/http-server';
|
|
61
|
+
|
|
62
|
+
const router = new Router();
|
|
63
|
+
|
|
64
|
+
router.get(
|
|
65
|
+
'/internal/revalidate',
|
|
66
|
+
requireAuth({
|
|
67
|
+
strategies: ['system'],
|
|
68
|
+
system: { secret: process.env.INTERNAL_API_SECRET, user: { id: 'system' } },
|
|
69
|
+
}),
|
|
70
|
+
handler
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
router.get(
|
|
74
|
+
'/me',
|
|
75
|
+
requireAuth({
|
|
76
|
+
strategies: ['jwt', 'apiToken'],
|
|
77
|
+
jwt: { secret: process.env.JWT_SECRET },
|
|
78
|
+
apiToken: { lookup },
|
|
79
|
+
}),
|
|
80
|
+
handler
|
|
81
|
+
);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Optional auth
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
app.use(
|
|
88
|
+
authMiddleware({
|
|
89
|
+
strategies: ['jwt'],
|
|
90
|
+
jwt: { secret: process.env.JWT_SECRET },
|
|
91
|
+
required: false, // unauthenticated requests pass through with ctx.state.user === undefined
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Context types
|
|
97
|
+
|
|
98
|
+
On successful authentication the middleware sets:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
ctx.state.user; // AuthenticatedUser
|
|
102
|
+
ctx.state.authStrategy; // 'jwt' | 'apiToken' | 'system'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
type AuthenticatedUser = {
|
|
107
|
+
id: string;
|
|
108
|
+
email?: string;
|
|
109
|
+
[key: string]: unknown;
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Behavior
|
|
114
|
+
|
|
115
|
+
1. Reads `Authorization: Bearer <token>`; missing header → 401 if `required`.
|
|
116
|
+
2. If `allowedOrigins` is configured and the `Origin` header doesn't match → 403. Requests without an Origin header are never rejected.
|
|
117
|
+
3. Tries each strategy in `strategies` order; first match wins.
|
|
118
|
+
- `jwt` — verifies HS256 JWT via `@ttoss/auth-core verifyJwt`; maps `sub`/`email` to user (override with `jwt.mapPayload`).
|
|
119
|
+
- `apiToken` — hashes the token (SHA-256) and calls `apiToken.lookup(hash)`.
|
|
120
|
+
- `system` — constant-time comparison against `system.secret`.
|
|
121
|
+
4. All strategies fail and `required` → 401. Failure reason is never leaked.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, {
|
|
3
|
+
value: 'Module'
|
|
4
|
+
});
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __create = Object.create;
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
11
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
15
|
+
key = keys[i];
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
17
|
+
__defProp(to, key, {
|
|
18
|
+
get: (k => from[k]).bind(null, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
27
|
+
value: mod,
|
|
28
|
+
enumerable: true
|
|
29
|
+
}) : target, mod));
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
let node_crypto = require("node:crypto");
|
|
33
|
+
node_crypto = __toESM(node_crypto, 1);
|
|
34
|
+
let _ttoss_auth_core = require("@ttoss/auth-core");
|
|
35
|
+
|
|
36
|
+
//#region src/origin.ts
|
|
37
|
+
/**
|
|
38
|
+
* Returns true if origin matches any entry in the allowlist.
|
|
39
|
+
* Strings are compared exactly; RegExps are tested.
|
|
40
|
+
*/
|
|
41
|
+
var isOriginAllowed = (origin, allowedOrigins) => {
|
|
42
|
+
for (const entry of allowedOrigins) {
|
|
43
|
+
if (entry === void 0) continue;
|
|
44
|
+
if (typeof entry === "string") {
|
|
45
|
+
if (entry === origin) return true;
|
|
46
|
+
} else if (entry.test(origin)) return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/authMiddleware.ts
|
|
53
|
+
var tryJwt = (token, opts) => {
|
|
54
|
+
const payload = (0, _ttoss_auth_core.verifyJwt)({
|
|
55
|
+
token,
|
|
56
|
+
secret: opts.secret
|
|
57
|
+
});
|
|
58
|
+
if (!payload) return null;
|
|
59
|
+
if (opts.mapPayload) return opts.mapPayload(payload);
|
|
60
|
+
return {
|
|
61
|
+
id: String(payload.sub ?? ""),
|
|
62
|
+
...(payload.email !== void 0 && {
|
|
63
|
+
email: String(payload.email)
|
|
64
|
+
})
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
var tryApiToken = async (token, opts) => {
|
|
68
|
+
return opts.lookup((0, _ttoss_auth_core.hashApiToken)(token));
|
|
69
|
+
};
|
|
70
|
+
var trySystem = (token, opts) => {
|
|
71
|
+
const a = Buffer.from(token);
|
|
72
|
+
const b = Buffer.from(opts.secret);
|
|
73
|
+
if (a.length !== b.length || !node_crypto.default.timingSafeEqual(a, b)) return null;
|
|
74
|
+
return opts.user;
|
|
75
|
+
};
|
|
76
|
+
var resolveUser = async (token, options) => {
|
|
77
|
+
for (const strategy of options.strategies) {
|
|
78
|
+
let user = null;
|
|
79
|
+
if (strategy === "jwt" && options.jwt) user = tryJwt(token, options.jwt);else if (strategy === "apiToken" && options.apiToken) user = await tryApiToken(token, options.apiToken);else if (strategy === "system" && options.system) user = trySystem(token, options.system);
|
|
80
|
+
if (user) return {
|
|
81
|
+
user,
|
|
82
|
+
strategy
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Koa middleware that authenticates requests via Bearer token.
|
|
89
|
+
* Supports JWT, hashed API tokens, and a shared system secret.
|
|
90
|
+
* Sets `ctx.state.user` and `ctx.state.authStrategy` on success.
|
|
91
|
+
*/
|
|
92
|
+
var authMiddleware = options => {
|
|
93
|
+
const required = options.required ?? true;
|
|
94
|
+
return async (ctx, next) => {
|
|
95
|
+
if (options.allowedOrigins) {
|
|
96
|
+
const origin = ctx.get("Origin");
|
|
97
|
+
if (origin && !isOriginAllowed(origin, options.allowedOrigins)) ctx.throw(403, "Invalid origin");
|
|
98
|
+
}
|
|
99
|
+
const authHeader = ctx.get("Authorization");
|
|
100
|
+
const token = authHeader && authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
101
|
+
if (!token) {
|
|
102
|
+
if (required) ctx.throw(401, "Unauthorized");
|
|
103
|
+
return next();
|
|
104
|
+
}
|
|
105
|
+
const result = await resolveUser(token, options);
|
|
106
|
+
if (!result) {
|
|
107
|
+
if (required) ctx.throw(401, "Unauthorized");
|
|
108
|
+
return next();
|
|
109
|
+
}
|
|
110
|
+
ctx.state.user = result.user;
|
|
111
|
+
ctx.state.authStrategy = result.strategy;
|
|
112
|
+
return next();
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/requireAuth.ts
|
|
118
|
+
/**
|
|
119
|
+
* Route-level authentication middleware. Same options as `authMiddleware`,
|
|
120
|
+
* `required` defaults to true.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* router.get('/me', requireAuth({ strategies: ['jwt', 'apiToken'] }), handler);
|
|
124
|
+
*/
|
|
125
|
+
var requireAuth = options => {
|
|
126
|
+
return authMiddleware({
|
|
127
|
+
required: true,
|
|
128
|
+
...options
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
exports.authMiddleware = authMiddleware;
|
|
134
|
+
exports.isOriginAllowed = isOriginAllowed;
|
|
135
|
+
exports.requireAuth = requireAuth;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
|
|
2
|
+
import { Context, Next } from "@ttoss/http-server";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
type AuthenticatedUser = {
|
|
6
|
+
id: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
type AuthStrategy = 'jwt' | 'apiToken' | 'system';
|
|
11
|
+
type JwtOptions = {
|
|
12
|
+
secret: string;
|
|
13
|
+
/**
|
|
14
|
+
* Override how the JWT payload maps to an AuthenticatedUser.
|
|
15
|
+
* Defaults to `{ id: payload.sub, email: payload.email }`.
|
|
16
|
+
*/
|
|
17
|
+
mapPayload?: (payload: Record<string, unknown>) => AuthenticatedUser | null;
|
|
18
|
+
};
|
|
19
|
+
type ApiTokenOptions = {
|
|
20
|
+
/**
|
|
21
|
+
* Receives the SHA-256 hash of the presented token and returns the
|
|
22
|
+
* authenticated user, or null if not found / revoked.
|
|
23
|
+
*/
|
|
24
|
+
lookup: (tokenHash: string) => Promise<AuthenticatedUser | null>;
|
|
25
|
+
};
|
|
26
|
+
type SystemOptions = {
|
|
27
|
+
secret: string; /** User attached to ctx.state.user for system calls. */
|
|
28
|
+
user: AuthenticatedUser;
|
|
29
|
+
};
|
|
30
|
+
type AuthMiddlewareOptions = {
|
|
31
|
+
/** Ordered list of strategies to attempt. First match wins. */strategies: AuthStrategy[];
|
|
32
|
+
jwt?: JwtOptions;
|
|
33
|
+
apiToken?: ApiTokenOptions;
|
|
34
|
+
system?: SystemOptions;
|
|
35
|
+
/**
|
|
36
|
+
* Optional origin allowlist. Strings are exact-matched; RegExps are tested.
|
|
37
|
+
* Requests without an Origin header are never rejected by this check.
|
|
38
|
+
*/
|
|
39
|
+
allowedOrigins?: Array<string | RegExp | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* When true (default), unauthenticated requests receive 401.
|
|
42
|
+
* When false, they pass through with ctx.state.user === undefined.
|
|
43
|
+
*/
|
|
44
|
+
required?: boolean;
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/authMiddleware.d.ts
|
|
48
|
+
/**
|
|
49
|
+
* Koa middleware that authenticates requests via Bearer token.
|
|
50
|
+
* Supports JWT, hashed API tokens, and a shared system secret.
|
|
51
|
+
* Sets `ctx.state.user` and `ctx.state.authStrategy` on success.
|
|
52
|
+
*/
|
|
53
|
+
declare const authMiddleware: (options: AuthMiddlewareOptions) => (ctx: Context, next: Next) => Promise<void>;
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/origin.d.ts
|
|
56
|
+
/**
|
|
57
|
+
* Returns true if origin matches any entry in the allowlist.
|
|
58
|
+
* Strings are compared exactly; RegExps are tested.
|
|
59
|
+
*/
|
|
60
|
+
declare const isOriginAllowed: (origin: string, allowedOrigins: Array<string | RegExp | undefined>) => boolean;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/requireAuth.d.ts
|
|
63
|
+
/**
|
|
64
|
+
* Route-level authentication middleware. Same options as `authMiddleware`,
|
|
65
|
+
* `required` defaults to true.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* router.get('/me', requireAuth({ strategies: ['jwt', 'apiToken'] }), handler);
|
|
69
|
+
*/
|
|
70
|
+
declare const requireAuth: (options: AuthMiddlewareOptions) => ((ctx: Context, next: Next) => Promise<void>);
|
|
71
|
+
//#endregion
|
|
72
|
+
export { type ApiTokenOptions, type AuthMiddlewareOptions, type AuthStrategy, type AuthenticatedUser, type JwtOptions, type SystemOptions, authMiddleware, isOriginAllowed, requireAuth };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
|
|
2
|
+
import { Context, Next } from "@ttoss/http-server";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
type AuthenticatedUser = {
|
|
6
|
+
id: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
type AuthStrategy = 'jwt' | 'apiToken' | 'system';
|
|
11
|
+
type JwtOptions = {
|
|
12
|
+
secret: string;
|
|
13
|
+
/**
|
|
14
|
+
* Override how the JWT payload maps to an AuthenticatedUser.
|
|
15
|
+
* Defaults to `{ id: payload.sub, email: payload.email }`.
|
|
16
|
+
*/
|
|
17
|
+
mapPayload?: (payload: Record<string, unknown>) => AuthenticatedUser | null;
|
|
18
|
+
};
|
|
19
|
+
type ApiTokenOptions = {
|
|
20
|
+
/**
|
|
21
|
+
* Receives the SHA-256 hash of the presented token and returns the
|
|
22
|
+
* authenticated user, or null if not found / revoked.
|
|
23
|
+
*/
|
|
24
|
+
lookup: (tokenHash: string) => Promise<AuthenticatedUser | null>;
|
|
25
|
+
};
|
|
26
|
+
type SystemOptions = {
|
|
27
|
+
secret: string; /** User attached to ctx.state.user for system calls. */
|
|
28
|
+
user: AuthenticatedUser;
|
|
29
|
+
};
|
|
30
|
+
type AuthMiddlewareOptions = {
|
|
31
|
+
/** Ordered list of strategies to attempt. First match wins. */strategies: AuthStrategy[];
|
|
32
|
+
jwt?: JwtOptions;
|
|
33
|
+
apiToken?: ApiTokenOptions;
|
|
34
|
+
system?: SystemOptions;
|
|
35
|
+
/**
|
|
36
|
+
* Optional origin allowlist. Strings are exact-matched; RegExps are tested.
|
|
37
|
+
* Requests without an Origin header are never rejected by this check.
|
|
38
|
+
*/
|
|
39
|
+
allowedOrigins?: Array<string | RegExp | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* When true (default), unauthenticated requests receive 401.
|
|
42
|
+
* When false, they pass through with ctx.state.user === undefined.
|
|
43
|
+
*/
|
|
44
|
+
required?: boolean;
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/authMiddleware.d.ts
|
|
48
|
+
/**
|
|
49
|
+
* Koa middleware that authenticates requests via Bearer token.
|
|
50
|
+
* Supports JWT, hashed API tokens, and a shared system secret.
|
|
51
|
+
* Sets `ctx.state.user` and `ctx.state.authStrategy` on success.
|
|
52
|
+
*/
|
|
53
|
+
declare const authMiddleware: (options: AuthMiddlewareOptions) => (ctx: Context, next: Next) => Promise<void>;
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/origin.d.ts
|
|
56
|
+
/**
|
|
57
|
+
* Returns true if origin matches any entry in the allowlist.
|
|
58
|
+
* Strings are compared exactly; RegExps are tested.
|
|
59
|
+
*/
|
|
60
|
+
declare const isOriginAllowed: (origin: string, allowedOrigins: Array<string | RegExp | undefined>) => boolean;
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/requireAuth.d.ts
|
|
63
|
+
/**
|
|
64
|
+
* Route-level authentication middleware. Same options as `authMiddleware`,
|
|
65
|
+
* `required` defaults to true.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* router.get('/me', requireAuth({ strategies: ['jwt', 'apiToken'] }), handler);
|
|
69
|
+
*/
|
|
70
|
+
declare const requireAuth: (options: AuthMiddlewareOptions) => ((ctx: Context, next: Next) => Promise<void>);
|
|
71
|
+
//#endregion
|
|
72
|
+
export { type ApiTokenOptions, type AuthMiddlewareOptions, type AuthStrategy, type AuthenticatedUser, type JwtOptions, type SystemOptions, authMiddleware, isOriginAllowed, requireAuth };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { hashApiToken, verifyJwt } from "@ttoss/auth-core";
|
|
4
|
+
|
|
5
|
+
//#region src/origin.ts
|
|
6
|
+
/**
|
|
7
|
+
* Returns true if origin matches any entry in the allowlist.
|
|
8
|
+
* Strings are compared exactly; RegExps are tested.
|
|
9
|
+
*/
|
|
10
|
+
var isOriginAllowed = (origin, allowedOrigins) => {
|
|
11
|
+
for (const entry of allowedOrigins) {
|
|
12
|
+
if (entry === void 0) continue;
|
|
13
|
+
if (typeof entry === "string") {
|
|
14
|
+
if (entry === origin) return true;
|
|
15
|
+
} else if (entry.test(origin)) return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/authMiddleware.ts
|
|
22
|
+
var tryJwt = (token, opts) => {
|
|
23
|
+
const payload = verifyJwt({
|
|
24
|
+
token,
|
|
25
|
+
secret: opts.secret
|
|
26
|
+
});
|
|
27
|
+
if (!payload) return null;
|
|
28
|
+
if (opts.mapPayload) return opts.mapPayload(payload);
|
|
29
|
+
return {
|
|
30
|
+
id: String(payload.sub ?? ""),
|
|
31
|
+
...(payload.email !== void 0 && {
|
|
32
|
+
email: String(payload.email)
|
|
33
|
+
})
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
var tryApiToken = async (token, opts) => {
|
|
37
|
+
return opts.lookup(hashApiToken(token));
|
|
38
|
+
};
|
|
39
|
+
var trySystem = (token, opts) => {
|
|
40
|
+
const a = Buffer.from(token);
|
|
41
|
+
const b = Buffer.from(opts.secret);
|
|
42
|
+
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return null;
|
|
43
|
+
return opts.user;
|
|
44
|
+
};
|
|
45
|
+
var resolveUser = async (token, options) => {
|
|
46
|
+
for (const strategy of options.strategies) {
|
|
47
|
+
let user = null;
|
|
48
|
+
if (strategy === "jwt" && options.jwt) user = tryJwt(token, options.jwt);else if (strategy === "apiToken" && options.apiToken) user = await tryApiToken(token, options.apiToken);else if (strategy === "system" && options.system) user = trySystem(token, options.system);
|
|
49
|
+
if (user) return {
|
|
50
|
+
user,
|
|
51
|
+
strategy
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Koa middleware that authenticates requests via Bearer token.
|
|
58
|
+
* Supports JWT, hashed API tokens, and a shared system secret.
|
|
59
|
+
* Sets `ctx.state.user` and `ctx.state.authStrategy` on success.
|
|
60
|
+
*/
|
|
61
|
+
var authMiddleware = options => {
|
|
62
|
+
const required = options.required ?? true;
|
|
63
|
+
return async (ctx, next) => {
|
|
64
|
+
if (options.allowedOrigins) {
|
|
65
|
+
const origin = ctx.get("Origin");
|
|
66
|
+
if (origin && !isOriginAllowed(origin, options.allowedOrigins)) ctx.throw(403, "Invalid origin");
|
|
67
|
+
}
|
|
68
|
+
const authHeader = ctx.get("Authorization");
|
|
69
|
+
const token = authHeader && authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
70
|
+
if (!token) {
|
|
71
|
+
if (required) ctx.throw(401, "Unauthorized");
|
|
72
|
+
return next();
|
|
73
|
+
}
|
|
74
|
+
const result = await resolveUser(token, options);
|
|
75
|
+
if (!result) {
|
|
76
|
+
if (required) ctx.throw(401, "Unauthorized");
|
|
77
|
+
return next();
|
|
78
|
+
}
|
|
79
|
+
ctx.state.user = result.user;
|
|
80
|
+
ctx.state.authStrategy = result.strategy;
|
|
81
|
+
return next();
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/requireAuth.ts
|
|
87
|
+
/**
|
|
88
|
+
* Route-level authentication middleware. Same options as `authMiddleware`,
|
|
89
|
+
* `required` defaults to true.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* router.get('/me', requireAuth({ strategies: ['jwt', 'apiToken'] }), handler);
|
|
93
|
+
*/
|
|
94
|
+
var requireAuth = options => {
|
|
95
|
+
return authMiddleware({
|
|
96
|
+
required: true,
|
|
97
|
+
...options
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
export { authMiddleware, isOriginAllowed, requireAuth };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ttoss/http-server-auth",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Authentication middleware for @ttoss/http-server",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"auth",
|
|
7
|
+
"authentication",
|
|
8
|
+
"koa",
|
|
9
|
+
"middleware",
|
|
10
|
+
"ttoss"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/ttoss/ttoss.git",
|
|
15
|
+
"directory": "packages/http-server-auth"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "ttoss",
|
|
19
|
+
"contributors": [
|
|
20
|
+
"Pedro Arantes <pedro@arantespp.com> (https://arantespp.com)"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": "./dist/index.mjs",
|
|
27
|
+
"require": "./dist/index.cjs",
|
|
28
|
+
"types": "./dist/index.d.mts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@ttoss/auth-core": "^0.5.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/koa": "^3.0.3",
|
|
39
|
+
"jest": "^30.4.2",
|
|
40
|
+
"supertest": "^7.2.2",
|
|
41
|
+
"tsdown": "^0.22.2",
|
|
42
|
+
"@ttoss/http-server": "^0.6.1",
|
|
43
|
+
"@ttoss/config": "^1.37.17"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@ttoss/http-server": "^0.6.1"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"provenance": true
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsdown",
|
|
54
|
+
"test": "jest --projects tests/unit"
|
|
55
|
+
}
|
|
56
|
+
}
|