@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 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;
@@ -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 };
@@ -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
+ }