@nanokajs/auth 1.6.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/CHANGELOG.md ADDED
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@nanokajs/auth` will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.6.0] — 2026-05-17
9
+
10
+ ### Added
11
+
12
+ - `BlacklistStore` interface に optional な `addWithSubject(jti, sub, expiresAt)` / `hasForSubject(jti, sub)` を追加。実装すれば `refreshHandler` が自動で sub 一致チェックを行い、jti 単独でのログアウト攻撃に対する防御の深さが向上する。既存実装は無影響(後方互換)。
13
+
14
+ ### Security
15
+
16
+ - `kvBlacklistStore` のキーを生 jti から `SHA-256(jti)` ハッシュへ変更。KV `list()` での jti 列挙を遮断。
17
+ - `kvBlacklistStore` に `addWithSubject` / `hasForSubject` を実装。値に sub を JSON 形式で格納。
18
+ - `refreshHandler` の `HTTPException(401, …)` から `cause` プロパティを削除し、verify 内部エラーがユーザーカスタム `onError` 経由でレスポンスに漏洩するリスクを遮断。`middleware()` 側も同様の対応。サーバー側ログには `console.warn` で残す。
19
+ - `refreshHandler` で cookie モードの空文字列 (`refresh_token=`) を `undefined` と同等扱いし、適切な fallback / 401 経路へ振り分ける。
20
+ - `refreshHandler` で `payload.jti` の長さ上限 256 文字、文字種 `[A-Za-z0-9_-]+` の検証を追加。KV キー上限と異常値混入対策。
21
+ - `jwt.ts` の `verify()` で `alg` 検証順序を整理。`'none'` を大文字小文字非依存で拒否してから `'HS256'` ホワイトリストを適用。
22
+
23
+ ### Breaking notes
24
+
25
+ - 既存の `kvBlacklistStore` で v1.5.x 以前に格納された KV エントリ(生 jti キー)は v1.6.0 移行後に無効化される。Workers 移行後、blacklist は実質クリア状態となる。短命なエントリ(refresh token TTL 期間内)のみ影響するため、ほとんどのユーザーは追加対応不要。
26
+
27
+ ## [1.5.1] — 2026-05-10
28
+
29
+ ### Fixed
30
+
31
+ - `authMiddleware`: reject tokens without `type: 'access'` claim. Previously a refresh token passed via `Authorization: Bearer` could pass through standalone `authMiddleware`, since the `type` claim check was only present in `createAuth().middleware()`. Now both code paths enforce `payload.type === 'access'` and return 401 otherwise. Affects users of standalone `authMiddleware` only; `createAuth().middleware()` was unaffected. Security fix; aligns runtime with the behavior already documented in the README.
32
+
33
+ <!--
34
+ [1.5.0] entry is missing here. v1.5.0 changes shipped without a changelog entry; the gap will be backfilled in a follow-up. See git history for the v1.5.0 release.
35
+ -->
36
+
37
+ ## [1.4.0] — 2026-05-07
38
+
39
+ ### Added
40
+
41
+ - `CookieOptions` interface and `cookie?` field on `CreateAuthOptions`.
42
+ - `loginHandler()`: when `cookie` is set, writes `access_token` and `refresh_token` as `Set-Cookie` headers (`HttpOnly`, `SameSite=Lax`, `Secure` by default) and returns `{ ok: true }` instead of tokens in the body.
43
+ - `refreshHandler()`: when `cookie` is set, reads the refresh token from the cookie (falls back to `body.refreshToken` if absent); on success writes the new access token via `Set-Cookie` and returns `{ ok: true }`.
44
+ - All cookie attributes (`httpOnly`, `sameSite`, `secure`, `path`, `accessTokenName`, `refreshTokenName`) are configurable with sensible secure defaults.
45
+ - Omitting `cookie` preserves the existing JSON body behavior — no breaking change.
46
+
47
+ ## [1.3.0] — 2026-05-07
48
+
49
+ ### Added
50
+
51
+ - `createAuth(opts)` — factory that returns `loginHandler()`, `refreshHandler()`, and `middleware()` from a single configuration object.
52
+ - `loginHandler()`: validates credentials against a `NanokaModel`, issues HS256 access and refresh tokens. Applies a timing-safe dummy hash comparison when the identifier is not found.
53
+ - `refreshHandler()`: accepts a refresh token and issues a new access token. Rejects access tokens used as refresh tokens.
54
+ - `middleware()`: thin wrapper around `authMiddleware({ secret })`.
55
+ - Configurable `hasher` (defaults to `pbkdf2Hasher`), `jwt.expiresIn` (default 900 s), and `jwt.refreshExpiresIn` (default 604 800 s).
56
+
57
+ ## [1.2.0] — 2026-05-06
58
+
59
+ ### Added
60
+
61
+ - `authMiddleware({ secret })`: Hono Bearer token validation middleware with generics + Hono `Variables` support.
62
+
63
+ ## [1.1.0] — 2026-05-05
64
+
65
+ ### Added
66
+
67
+ - `sign` / `verify`: HS256 JWT utilities using `crypto.subtle` (zero-dependency).
68
+
69
+ ## [1.0.0] — 2026-05-04
70
+
71
+ ### Added
72
+
73
+ - `Hasher` interface (`hash` / `verify`).
74
+ - `pbkdf2Hasher`: zero-dependency PBKDF2 implementation using `crypto.subtle` (SHA-256, 310 000 iterations, timing-safe comparison).
package/README.md ADDED
@@ -0,0 +1,213 @@
1
+ # @nanokajs/auth
2
+
3
+ Authentication utilities for Nanoka on Cloudflare Workers.
4
+
5
+ Provides credential validation, JWT issuance, and Hono middleware — built on `crypto.subtle`, zero runtime dependencies beyond Hono.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add @nanokajs/auth
11
+ ```
12
+
13
+ Peer dependencies: `@nanokajs/core ^1.0.0`, `hono ^4.0.0`.
14
+
15
+ ## Usage
16
+
17
+ ### Basic (JSON body)
18
+
19
+ ```ts
20
+ import { createAuth } from '@nanokajs/auth'
21
+ import { nanoka, d1Adapter } from '@nanokajs/core'
22
+
23
+ const app = nanoka(d1Adapter(env.DB))
24
+ const User = app.model('users', { /* fields */ })
25
+
26
+ const auth = createAuth({
27
+ model: User,
28
+ secret: env.AUTH_SECRET, // must be >= 32 chars
29
+ fields: { identifier: 'email', password: 'passwordHash' },
30
+ })
31
+
32
+ app.post('/login', auth.loginHandler()) // returns { accessToken, refreshToken }
33
+ app.post('/refresh', auth.refreshHandler()) // returns { accessToken }
34
+ app.use('/api/*', auth.middleware())
35
+ ```
36
+
37
+ ### Cookie mode
38
+
39
+ Pass `cookie: {}` to enable HttpOnly cookie delivery. The `cookie` option is entirely optional — omitting it preserves the existing JSON body behavior.
40
+
41
+ ```ts
42
+ const auth = createAuth({
43
+ model: User,
44
+ secret: env.AUTH_SECRET,
45
+ fields: { identifier: 'email', password: 'passwordHash' },
46
+ cookie: {
47
+ httpOnly: true, // default: true
48
+ sameSite: 'Lax', // default: 'Lax' ('Strict' | 'Lax' | 'None')
49
+ secure: true, // default: true
50
+ path: '/', // default: '/'
51
+ accessTokenName: 'access_token', // default: 'access_token'
52
+ refreshTokenName: 'refresh_token', // default: 'refresh_token'
53
+ },
54
+ })
55
+ ```
56
+
57
+ When `cookie` is set:
58
+
59
+ - `loginHandler()` sets two `Set-Cookie` headers (`access_token`, `refresh_token`) and returns `{ ok: true }` — tokens are not in the response body.
60
+ - `refreshHandler()` reads the refresh token from the `refresh_token` cookie. If the cookie is absent and `jwt.rotation` is `false`, it falls back to `body.refreshToken` to support non-browser clients. When `jwt.rotation` is `true`, the body fallback is **disabled** — the cookie must be present or the request is rejected with 401. This prevents XSS-obtained refresh tokens from bypassing the `SameSite` cookie defence by sending them directly as JSON.
61
+ - On successful refresh, the new access token is written back via `Set-Cookie` and the body is `{ ok: true }`. When `jwt.rotation` is `true`, the refresh token is also rotated and written back via `Set-Cookie: refresh_token=<new>`. When rotation is disabled (the default), the `refresh_token` cookie is not overwritten.
62
+
63
+ ```ts
64
+ app.post('/login', auth.loginHandler())
65
+ app.post('/refresh', auth.refreshHandler())
66
+ ```
67
+
68
+ ## Refresh token rotation
69
+
70
+ Refresh token rotation replaces the refresh token on every use, making stolen token reuse detectable on a best-effort basis.
71
+
72
+ > **Note:** With the KV-backed `BlacklistStore`, detection is eventually consistent — a small race window exists where two simultaneous refreshes from different regions may both succeed. For strong consistency, see Issue #115 (D1-backed store, planned).
73
+
74
+ ### Enabling rotation
75
+
76
+ ```ts
77
+ import { createAuth, kvBlacklistStore } from '@nanokajs/auth'
78
+
79
+ const auth = createAuth({
80
+ model: User,
81
+ secret: env.AUTH_SECRET,
82
+ fields: { identifier: 'email', password: 'passwordHash' },
83
+ jwt: { rotation: true },
84
+ blacklist: kvBlacklistStore(env.REFRESH_BLACKLIST_KV),
85
+ })
86
+ ```
87
+
88
+ - `jwt.rotation` defaults to `false`. When rotation is disabled, refresh tokens remain valid until their `exp` and can be reused any number of times. If stolen token detection is required, enable `rotation: true` together with a `BlacklistStore`.
89
+ - `blacklist` is **required** when `rotation: true`. Passing `rotation: true` without `blacklist` throws an error at `createAuth` call time (fail-fast).
90
+ - When rotation is enabled, `loginHandler` embeds a `jti` claim in the refresh token. `refreshHandler` verifies the `jti`, rejects it if it is already blacklisted, blacklists the old `jti`, and issues a new refresh token with a fresh `jti`.
91
+
92
+ ### Response shape
93
+
94
+ | Mode | `refreshHandler` response |
95
+ |---|---|
96
+ | rotation disabled (default) | `{ accessToken }` |
97
+ | rotation enabled (JSON body) | `{ accessToken, refreshToken }` |
98
+ | rotation enabled (cookie mode) | `{ ok: true }` + `Set-Cookie: access_token=<new>; Set-Cookie: refresh_token=<new>` |
99
+
100
+ ### KV blacklist store
101
+
102
+ `kvBlacklistStore(kv, opts?)` stores blacklisted JTIs in a Workers KV namespace. TTL is derived from the token's `exp` claim, with a minimum of 60 seconds (KV lower bound).
103
+
104
+ ```ts
105
+ kvBlacklistStore(env.REFRESH_BLACKLIST_KV, { prefix: 'my-app:bl:' })
106
+ ```
107
+
108
+ | Option | Default |
109
+ |---|---|
110
+ | `prefix` | `'nanoka-auth:bl:'` |
111
+
112
+ KV keys are stored as `SHA-256(jti)` hex strings, not the raw JTI. This prevents JTI enumeration via KV `list()`.
113
+
114
+ A D1-backed blacklist store is planned as a follow-up.
115
+
116
+ ### BlacklistStore interface
117
+
118
+ `BlacklistStore` is the interface that any custom blacklist implementation must satisfy.
119
+
120
+ ```ts
121
+ export interface BlacklistStore {
122
+ add(jti: string, expiresAt: number): Promise<void>
123
+ has(jti: string): Promise<boolean>
124
+ // Optional: defense-in-depth pair-check with sub.
125
+ // If implemented, refreshHandler uses it instead of has/add.
126
+ addWithSubject?(jti: string, sub: string, expiresAt: number): Promise<void>
127
+ hasForSubject?(jti: string, sub: string): Promise<boolean>
128
+ }
129
+ ```
130
+
131
+ | Method | Required | Description |
132
+ |---|---|---|
133
+ | `add(jti, expiresAt)` | Yes | Blacklist a JTI until `expiresAt` (Unix seconds) |
134
+ | `has(jti)` | Yes | Return `true` if the JTI is blacklisted |
135
+ | `addWithSubject(jti, sub, expiresAt)` | No (optional) | Blacklist a JTI paired with a `sub`. If implemented, `refreshHandler` calls this instead of `add`. |
136
+ | `hasForSubject(jti, sub)` | No (optional) | Return `true` only if the JTI is blacklisted **and** was associated with the given `sub`. If implemented, `refreshHandler` calls this instead of `has`. |
137
+
138
+ Implementing `addWithSubject` / `hasForSubject` provides defense-in-depth: even if an attacker obtains a valid JTI, they cannot reuse it under a different `sub`. Existing implementations that only implement `add` / `has` continue to work without any changes.
139
+
140
+ > **Note**: It is recommended to implement `addWithSubject` and `hasForSubject` as a pair. If only one of the two is implemented, the missing side falls back to the existing `add` / `has` path, and the sub-matching defense-in-depth check will not be applied.
141
+
142
+ > **Cross-sub overwrite trade-off**: `addWithSubject` writes a single entry keyed by `jti` (the value carries the `sub`). If an attacker who somehow obtains a valid `jti` calls `refreshHandler` under a different `sub`, the existing blacklist entry for the original `sub` is overwritten. This is a known design trade-off of single-key blacklists and is tracked for further hardening in [Issue #115](https://github.com/nanokajs/nanoka/issues/115) (D1-backed store). In normal threat models the attacker cannot mint refresh tokens without the signing secret, so this remains a defense-in-depth concern rather than an exploitable bypass.
143
+
144
+ ## Rate limiting
145
+
146
+ `loginHandler()` does not include built-in rate limiting or brute-force protection. Protect the login endpoint at the infrastructure or middleware layer before deploying to production. Options include:
147
+
148
+ - Cloudflare WAF rate limiting rules (recommended for Workers deployments)
149
+ - A Hono middleware that tracks failed attempts per IP or identifier and returns `429 Too Many Requests`
150
+
151
+ Without rate limiting, an attacker can enumerate passwords against any valid identifier at the speed the Worker allows.
152
+
153
+ ## CSRF considerations
154
+
155
+ `middleware()` only accepts `Authorization: Bearer <token>`. Cookie-based access tokens are **not** read by the middleware — they are intended for browser clients that forward the cookie through a backend-for-frontend (BFF) pattern, or for setups where `httpOnly: false` is explicitly configured so that JavaScript can read the token and attach it as a Bearer header.
156
+
157
+ When using `SameSite` cookies for browser flows, apply CSRF protection appropriate to your deployment:
158
+
159
+ | Scenario | Recommended approach |
160
+ |---|---|
161
+ | Same-site (same eTLD+1) | `sameSite: 'Strict'` — browser refuses cross-site cookie send |
162
+ | Cross-origin API | `sameSite: 'Lax'` + Double Submit Cookie (send a readable CSRF token in a separate non-HttpOnly cookie; validate it server-side on state-changing requests) |
163
+
164
+ `SameSite: None` requires `secure: true` and is only needed for explicitly cross-site embedded contexts. Avoid it unless you have a specific cross-origin embedding requirement.
165
+
166
+ ## API reference
167
+
168
+ ### `createAuth(opts)`
169
+
170
+ | Option | Type | Default | Description |
171
+ |---|---|---|---|
172
+ | `model` | `NanokaModel<any>` | required | The user model |
173
+ | `secret` | `string` | required | HS256 signing secret (min 32 chars) |
174
+ | `fields.identifier` | `string` | required | Field name used as the login identifier (e.g. `'email'`) |
175
+ | `fields.password` | `string` | required | Field name storing the password hash |
176
+ | `hasher` | `Hasher` | `pbkdf2Hasher` | Custom hasher interface (`hash` / `verify`) |
177
+ | `jwt.expiresIn` | `number` | `900` | Access token TTL in seconds |
178
+ | `jwt.refreshExpiresIn` | `number` | `604800` | Refresh token TTL in seconds |
179
+ | `jwt.rotation` | `boolean` | `false` | Enable refresh token rotation (requires `blacklist`) |
180
+ | `blacklist` | `BlacklistStore` | `undefined` | Blacklist store for rotation (required when `rotation: true`) |
181
+ | `cookie` | `CookieOptions` | `undefined` | Cookie delivery options (see below) |
182
+
183
+ ### `CookieOptions`
184
+
185
+ | Field | Type | Default |
186
+ |---|---|---|
187
+ | `httpOnly` | `boolean` | `true` |
188
+ | `sameSite` | `'Strict' \| 'Lax' \| 'None'` | `'Lax'` |
189
+ | `secure` | `boolean` | `true` |
190
+ | `path` | `string` | `'/'` |
191
+ | `accessTokenName` | `string` | `'access_token'` |
192
+ | `refreshTokenName` | `string` | `'refresh_token'` |
193
+
194
+ ### `authMiddleware({ secret })`
195
+
196
+ Standalone middleware (also available as `auth.middleware()`). Reads `Authorization: Bearer <token>`, verifies the HS256 signature, and calls `c.set('user', payload)`.
197
+
198
+ Rejects requests that:
199
+ - have no `Authorization` header
200
+ - use a scheme other than `Bearer` (case-insensitive)
201
+ - carry an expired or tampered token
202
+ - carry a refresh token where an access token is expected
203
+
204
+ ## Limitations
205
+
206
+ - **`Set-Cookie` overwrite is browser-dependent.** When rotation is enabled in cookie mode, `refreshHandler` writes a new `refresh_token` via `Set-Cookie`. Whether the browser replaces the old cookie depends on cookie attribute matching (name, domain, path, secure). Ensure your `cookie` options are consistent across all endpoints.
207
+ - **No built-in rate limiting.** `loginHandler` does not throttle requests. Protect the endpoint at the infrastructure layer.
208
+ - **D1 blacklist store not yet available.** The current blacklist implementation is KV-only. A D1-backed implementation is planned as a follow-up issue.
209
+
210
+ ## Security notes
211
+
212
+ - **Do not expose `HTTPException.cause` in responses or logs.** Starting from v1.6.0, `@nanokajs/auth` no longer sets `cause` on the `HTTPException` it throws, so internal verify errors no longer surface through the default Hono `onError` handler. However, if you implement a custom `onError` or exception filter, be careful not to serialize `cause` or stack traces into responses — the same principle applies to any exceptions you throw yourself.
213
+ - **KV keys are hashed.** `kvBlacklistStore` stores `SHA-256(jti)` as the KV key. Raw JTI values are never written to KV keys, preventing JTI enumeration via `kv.list()`.
@@ -0,0 +1,78 @@
1
+ import { KVNamespace } from '@cloudflare/workers-types';
2
+ import { NanokaModel } from '@nanokajs/core';
3
+ import { Handler, MiddlewareHandler } from 'hono';
4
+
5
+ interface BlacklistStore {
6
+ add(jti: string, expiresAt: number): Promise<void>;
7
+ has(jti: string): Promise<boolean>;
8
+ addWithSubject?(jti: string, sub: string, expiresAt: number): Promise<void>;
9
+ hasForSubject?(jti: string, sub: string): Promise<boolean>;
10
+ }
11
+
12
+ declare function kvBlacklistStore(kv: KVNamespace, opts?: {
13
+ prefix?: string;
14
+ }): BlacklistStore;
15
+
16
+ interface Hasher {
17
+ hash(plain: string): Promise<string>;
18
+ verify(plain: string, stored: string): Promise<boolean>;
19
+ }
20
+
21
+ interface CookieOptions {
22
+ httpOnly?: boolean;
23
+ sameSite?: 'Strict' | 'Lax' | 'None';
24
+ secure?: boolean;
25
+ path?: string;
26
+ accessTokenName?: string;
27
+ refreshTokenName?: string;
28
+ }
29
+ interface CreateAuthOptions {
30
+ model: NanokaModel<any>;
31
+ secret: string;
32
+ fields: {
33
+ identifier: string;
34
+ password: string;
35
+ };
36
+ hasher?: Hasher;
37
+ jwt?: {
38
+ expiresIn?: number;
39
+ refreshExpiresIn?: number;
40
+ rotation?: boolean;
41
+ };
42
+ cookie?: CookieOptions;
43
+ blacklist?: BlacklistStore;
44
+ }
45
+ interface AuthInstance {
46
+ loginHandler(): Handler;
47
+ refreshHandler(): Handler;
48
+ middleware(): MiddlewareHandler;
49
+ }
50
+ declare function createAuth(opts: CreateAuthOptions): AuthInstance;
51
+
52
+ declare const pbkdf2Hasher: Hasher;
53
+
54
+ interface SignOptions {
55
+ expiresIn?: number;
56
+ }
57
+ /**
58
+ * HS256 で JWT を署名する。
59
+ * @remarks アルゴリズムは HS256 固定。RS256 / ES256 等の非対称アルゴリズムを追加する場合は
60
+ * 鍵オブジェクト型を分離し、verify に期待する alg を引数で渡す API に変更すること(alg confusion 攻撃防止)。
61
+ */
62
+ declare function sign(payload: Record<string, unknown>, secret: string, options?: SignOptions): Promise<string>;
63
+ /**
64
+ * HS256 JWT を検証してペイロードを返す。
65
+ * @remarks 戻り値の型パラメータ T は実行時検証なし。受け取った payload は Zod 等で検証すること。
66
+ * `nbf` / `iat` / `aud` / `iss` クレームは検証しない。必要な場合はアプリ側で payload を検証すること。
67
+ */
68
+ declare function verify<T = Record<string, unknown>>(token: string, secret: string): Promise<T>;
69
+
70
+ declare function authMiddleware<TPayload extends Record<string, unknown> = Record<string, unknown>>(opts: {
71
+ secret: string;
72
+ }): MiddlewareHandler<{
73
+ Variables: {
74
+ user: TPayload;
75
+ };
76
+ }>;
77
+
78
+ export { type AuthInstance, type BlacklistStore, type CookieOptions, type CreateAuthOptions, type Hasher, type SignOptions, authMiddleware, createAuth, kvBlacklistStore, pbkdf2Hasher, sign, verify };
package/dist/index.js ADDED
@@ -0,0 +1,445 @@
1
+ import { getCookie, setCookie } from 'hono/cookie';
2
+ import { HTTPException } from 'hono/http-exception';
3
+
4
+ // src/blacklist-stores/kv.ts
5
+ async function hashKey(jti) {
6
+ const encoder = new TextEncoder();
7
+ const buf = await crypto.subtle.digest("SHA-256", encoder.encode(jti));
8
+ const bytes = new Uint8Array(buf);
9
+ let hex = "";
10
+ for (let i = 0; i < bytes.length; i++) {
11
+ hex += bytes[i].toString(16).padStart(2, "0");
12
+ }
13
+ return hex;
14
+ }
15
+ function kvBlacklistStore(kv, opts) {
16
+ const prefix = opts?.prefix ?? "nanoka-auth:bl:";
17
+ return {
18
+ async add(jti, expiresAt) {
19
+ const ttl = Math.min(2592e3, Math.max(60, expiresAt - Math.floor(Date.now() / 1e3)));
20
+ await kv.put(`${prefix}${await hashKey(jti)}`, "1", { expirationTtl: ttl });
21
+ },
22
+ async has(jti) {
23
+ return await kv.get(`${prefix}${await hashKey(jti)}`) !== null;
24
+ },
25
+ async addWithSubject(jti, sub, expiresAt) {
26
+ const ttl = Math.min(2592e3, Math.max(60, expiresAt - Math.floor(Date.now() / 1e3)));
27
+ await kv.put(`${prefix}${await hashKey(jti)}`, JSON.stringify({ sub }), {
28
+ expirationTtl: ttl
29
+ });
30
+ },
31
+ async hasForSubject(jti, sub) {
32
+ const value = await kv.get(`${prefix}${await hashKey(jti)}`);
33
+ if (value === null) return false;
34
+ if (value === "1") return true;
35
+ try {
36
+ const parsed = JSON.parse(value);
37
+ if (typeof parsed === "object" && parsed !== null && "sub" in parsed) {
38
+ return parsed.sub === sub;
39
+ }
40
+ } catch {
41
+ }
42
+ return true;
43
+ }
44
+ };
45
+ }
46
+
47
+ // src/hashers/pbkdf2.ts
48
+ var ITERATIONS = 31e4;
49
+ var SALT_BYTES = 16;
50
+ var HASH_BYTES = 32;
51
+ var HASH_NAME = "SHA-256";
52
+ function utf8(str) {
53
+ return new TextEncoder().encode(str);
54
+ }
55
+ function toBase64url(buf) {
56
+ const bytes = new Uint8Array(buf);
57
+ let binary = "";
58
+ for (let i = 0; i < bytes.length; i++) {
59
+ binary += String.fromCharCode(bytes[i]);
60
+ }
61
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
62
+ }
63
+ function fromBase64url(str) {
64
+ try {
65
+ const padded = str.replace(/-/g, "+").replace(/_/g, "/").padEnd(str.length + (4 - str.length % 4) % 4, "=");
66
+ const binary = atob(padded);
67
+ const buf = new ArrayBuffer(binary.length);
68
+ const bytes = new Uint8Array(buf);
69
+ for (let i = 0; i < binary.length; i++) {
70
+ bytes[i] = binary.charCodeAt(i);
71
+ }
72
+ return bytes;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+ async function derive(plain, salt, iterations) {
78
+ const key = await crypto.subtle.importKey("raw", utf8(plain), "PBKDF2", false, ["deriveBits"]);
79
+ return crypto.subtle.deriveBits(
80
+ { name: "PBKDF2", salt, iterations, hash: HASH_NAME },
81
+ key,
82
+ HASH_BYTES * 8
83
+ );
84
+ }
85
+ function timingSafeEqual(a, b) {
86
+ if (a.length !== b.length) return false;
87
+ let diff = 0;
88
+ for (let i = 0; i < a.length; i++) {
89
+ diff |= a[i] ^ b[i];
90
+ }
91
+ return diff === 0;
92
+ }
93
+ var pbkdf2Hasher = {
94
+ async hash(plain) {
95
+ const saltBuf = new ArrayBuffer(SALT_BYTES);
96
+ const salt = new Uint8Array(saltBuf);
97
+ crypto.getRandomValues(salt);
98
+ const derived = await derive(plain, salt, ITERATIONS);
99
+ return `$pbkdf2$${ITERATIONS}$${toBase64url(saltBuf)}$${toBase64url(derived)}`;
100
+ },
101
+ async verify(plain, stored) {
102
+ const parts = stored.split("$");
103
+ if (parts.length !== 5 || parts[0] !== "") return false;
104
+ const algoId = parts[1];
105
+ const iterStr = parts[2];
106
+ const saltB64 = parts[3];
107
+ const hashB64 = parts[4];
108
+ if (algoId !== "pbkdf2") return false;
109
+ if (iterStr === void 0 || saltB64 === void 0 || hashB64 === void 0) return false;
110
+ const iterations = Number.parseInt(iterStr, 10);
111
+ if (!Number.isFinite(iterations) || iterations <= 0 || iterations > 1e6) return false;
112
+ const salt = fromBase64url(saltB64);
113
+ const storedHash = fromBase64url(hashB64);
114
+ if (salt === null || storedHash === null) return false;
115
+ if (salt.length === 0 || storedHash.length !== HASH_BYTES) return false;
116
+ const derived = await derive(plain, salt, iterations);
117
+ const derivedBytes = new Uint8Array(derived);
118
+ return timingSafeEqual(derivedBytes, storedHash);
119
+ }
120
+ };
121
+
122
+ // src/jwt.ts
123
+ var ALGORITHM = "HS256";
124
+ function utf82(str) {
125
+ return new TextEncoder().encode(str);
126
+ }
127
+ function toBase64url2(buf) {
128
+ const bytes = new Uint8Array(buf);
129
+ let binary = "";
130
+ for (let i = 0; i < bytes.length; i++) {
131
+ binary += String.fromCharCode(bytes[i]);
132
+ }
133
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
134
+ }
135
+ function fromBase64url2(str) {
136
+ const padded = str.replace(/-/g, "+").replace(/_/g, "/").padEnd(str.length + (4 - str.length % 4) % 4, "=");
137
+ const binary = atob(padded);
138
+ const buf = new ArrayBuffer(binary.length);
139
+ const bytes = new Uint8Array(buf);
140
+ for (let i = 0; i < binary.length; i++) {
141
+ bytes[i] = binary.charCodeAt(i);
142
+ }
143
+ return buf;
144
+ }
145
+ async function importKey(secret) {
146
+ return crypto.subtle.importKey("raw", utf82(secret), { name: "HMAC", hash: "SHA-256" }, false, [
147
+ "sign",
148
+ "verify"
149
+ ]);
150
+ }
151
+ async function sign(payload, secret, options) {
152
+ if (!secret) throw new Error("secret must not be empty");
153
+ const header = { alg: ALGORITHM, typ: "JWT" };
154
+ const headerB64 = toBase64url2(utf82(JSON.stringify(header)).buffer);
155
+ if (options?.expiresIn !== void 0 && !Number.isFinite(options.expiresIn)) {
156
+ throw new Error("expiresIn must be a finite number");
157
+ }
158
+ const finalPayload = options?.expiresIn !== void 0 ? { ...payload, exp: Math.floor(Date.now() / 1e3) + options.expiresIn } : payload;
159
+ const payloadB64 = toBase64url2(utf82(JSON.stringify(finalPayload)).buffer);
160
+ const signingInput = `${headerB64}.${payloadB64}`;
161
+ const key = await importKey(secret);
162
+ const signatureBuf = await crypto.subtle.sign("HMAC", key, utf82(signingInput));
163
+ const signatureB64 = toBase64url2(signatureBuf);
164
+ return `${signingInput}.${signatureB64}`;
165
+ }
166
+ async function verify(token, secret) {
167
+ if (!secret) throw new Error("secret must not be empty");
168
+ const segments = token.split(".");
169
+ if (segments.length !== 3) {
170
+ throw new Error("Invalid token format");
171
+ }
172
+ const [headerB64, payloadB64, signatureB64] = segments;
173
+ const header = JSON.parse(new TextDecoder().decode(fromBase64url2(headerB64)));
174
+ if (typeof header !== "object" || header === null || Array.isArray(header)) {
175
+ throw new Error("Invalid token header");
176
+ }
177
+ if (header.typ !== "JWT") {
178
+ throw new Error("Invalid token header");
179
+ }
180
+ const alg = header.alg;
181
+ if (typeof alg !== "string" || alg.toLowerCase() === "none") {
182
+ throw new Error("Invalid token header");
183
+ }
184
+ if (alg !== ALGORITHM) {
185
+ throw new Error("Invalid token header");
186
+ }
187
+ const signingInput = `${headerB64}.${payloadB64}`;
188
+ const key = await importKey(secret);
189
+ const signatureBuf = fromBase64url2(signatureB64);
190
+ const valid = await crypto.subtle.verify("HMAC", key, signatureBuf, utf82(signingInput));
191
+ if (!valid) {
192
+ throw new Error("Invalid signature");
193
+ }
194
+ const payload = JSON.parse(new TextDecoder().decode(fromBase64url2(payloadB64)));
195
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
196
+ throw new Error("Invalid payload");
197
+ }
198
+ const payloadObj = payload;
199
+ if (payloadObj.exp !== void 0) {
200
+ if (typeof payloadObj.exp !== "number" || !Number.isFinite(payloadObj.exp)) {
201
+ throw new Error("Invalid exp claim");
202
+ }
203
+ if (Math.floor(Date.now() / 1e3) >= payloadObj.exp) {
204
+ throw new Error("Token expired");
205
+ }
206
+ }
207
+ return payloadObj;
208
+ }
209
+
210
+ // src/create-auth.ts
211
+ function createAuth(opts) {
212
+ if (opts.secret.length < 32) {
213
+ throw new Error("createAuth: secret must be at least 32 characters");
214
+ }
215
+ const rotation = opts.jwt?.rotation ?? false;
216
+ if (rotation && opts.blacklist === void 0) {
217
+ throw new Error("createAuth: blacklist is required when jwt.rotation is true");
218
+ }
219
+ const hasher = opts.hasher ?? pbkdf2Hasher;
220
+ const expiresIn = opts.jwt?.expiresIn ?? 900;
221
+ const refreshExpiresIn = opts.jwt?.refreshExpiresIn ?? 604800;
222
+ const identifierField = opts.fields.identifier;
223
+ const passwordField = opts.fields.password;
224
+ const dummyHashPromise = hasher.hash("__dummy__");
225
+ const cookieOpts = opts.cookie;
226
+ const accessTokenName = cookieOpts?.accessTokenName ?? "access_token";
227
+ const refreshTokenName = cookieOpts?.refreshTokenName ?? "refresh_token";
228
+ return {
229
+ loginHandler() {
230
+ return async (c) => {
231
+ let body;
232
+ try {
233
+ body = await c.req.json();
234
+ } catch {
235
+ throw new HTTPException(401, { message: "Invalid credentials" });
236
+ }
237
+ const identifierValue = body[identifierField];
238
+ const passwordValue = body[passwordField];
239
+ if (typeof identifierValue !== "string" || typeof passwordValue !== "string") {
240
+ throw new HTTPException(401, { message: "Invalid credentials" });
241
+ }
242
+ const user = await opts.model.findOne({ [identifierField]: identifierValue });
243
+ if (user === null) {
244
+ const dummyHash = await dummyHashPromise;
245
+ await hasher.verify("__dummy__", dummyHash);
246
+ throw new HTTPException(401, { message: "Invalid credentials" });
247
+ }
248
+ const storedHash = user[passwordField];
249
+ const valid = await hasher.verify(passwordValue, storedHash);
250
+ if (!valid) {
251
+ throw new HTTPException(401, { message: "Invalid credentials" });
252
+ }
253
+ const sub = String(user.id ?? user[identifierField]);
254
+ const refreshPayload = { sub, type: "refresh" };
255
+ if (rotation) {
256
+ refreshPayload.jti = crypto.randomUUID();
257
+ }
258
+ const [accessToken, refreshToken] = await Promise.all([
259
+ sign({ sub, type: "access" }, opts.secret, { expiresIn }),
260
+ sign(refreshPayload, opts.secret, { expiresIn: refreshExpiresIn })
261
+ ]);
262
+ if (cookieOpts !== void 0) {
263
+ const sameSite = cookieOpts.sameSite ?? "Lax";
264
+ const baseCookieOptions = {
265
+ httpOnly: cookieOpts.httpOnly ?? true,
266
+ sameSite,
267
+ secure: sameSite === "None" ? true : cookieOpts.secure ?? true,
268
+ path: cookieOpts.path ?? "/"
269
+ };
270
+ setCookie(c, accessTokenName, accessToken, {
271
+ ...baseCookieOptions,
272
+ maxAge: expiresIn
273
+ });
274
+ setCookie(c, refreshTokenName, refreshToken, {
275
+ ...baseCookieOptions,
276
+ maxAge: refreshExpiresIn
277
+ });
278
+ return c.json({ ok: true }, 200);
279
+ }
280
+ return c.json({ accessToken, refreshToken }, 200);
281
+ };
282
+ },
283
+ refreshHandler() {
284
+ return async (c) => {
285
+ let refreshToken;
286
+ if (cookieOpts !== void 0) {
287
+ refreshToken = getCookie(c, refreshTokenName);
288
+ if ((refreshToken === void 0 || refreshToken === "") && rotation) {
289
+ throw new HTTPException(401, { message: "Invalid credentials" });
290
+ }
291
+ }
292
+ if (refreshToken === void 0 || refreshToken === "") {
293
+ let body;
294
+ try {
295
+ body = await c.req.json();
296
+ } catch {
297
+ throw new HTTPException(401, { message: "Invalid credentials" });
298
+ }
299
+ const bodyRefreshToken = body.refreshToken;
300
+ if (typeof bodyRefreshToken !== "string") {
301
+ throw new HTTPException(401, { message: "Invalid credentials" });
302
+ }
303
+ refreshToken = bodyRefreshToken;
304
+ }
305
+ let payload;
306
+ try {
307
+ payload = await verify(refreshToken, opts.secret);
308
+ } catch (err) {
309
+ console.warn("refresh token verify failed", err);
310
+ throw new HTTPException(401, { message: "Invalid credentials" });
311
+ }
312
+ if (payload.type !== "refresh") {
313
+ throw new HTTPException(401, { message: "Invalid credentials" });
314
+ }
315
+ if (typeof payload.sub !== "string" || payload.sub.length === 0) {
316
+ throw new HTTPException(401, { message: "Invalid credentials" });
317
+ }
318
+ if (rotation) {
319
+ const jti = payload.jti;
320
+ if (typeof jti !== "string" || jti.length === 0) {
321
+ throw new HTTPException(401, { message: "Invalid credentials" });
322
+ }
323
+ if (jti.length > 256) {
324
+ throw new HTTPException(401, { message: "Invalid credentials" });
325
+ }
326
+ if (!/^[A-Za-z0-9_-]+$/.test(jti)) {
327
+ throw new HTTPException(401, { message: "Invalid credentials" });
328
+ }
329
+ const blacklist = opts.blacklist;
330
+ const useSubjectAware = blacklist.hasForSubject !== void 0 && blacklist.addWithSubject !== void 0;
331
+ const blacklisted = useSubjectAware ? await blacklist.hasForSubject(jti, payload.sub) : await blacklist.has(jti);
332
+ if (blacklisted) {
333
+ throw new HTTPException(401, { message: "Invalid credentials" });
334
+ }
335
+ const exp = typeof payload.exp === "number" ? payload.exp : Math.floor(Date.now() / 1e3) + refreshExpiresIn;
336
+ try {
337
+ if (useSubjectAware) {
338
+ await blacklist.addWithSubject(jti, payload.sub, exp);
339
+ } else {
340
+ await blacklist.add(jti, exp);
341
+ }
342
+ } catch (err) {
343
+ console.error("blacklist.add failed", err);
344
+ throw new HTTPException(401, { message: "Invalid credentials" });
345
+ }
346
+ const newJti = crypto.randomUUID();
347
+ const [accessToken2, newRefreshToken] = await Promise.all([
348
+ sign({ sub: payload.sub, type: "access" }, opts.secret, { expiresIn }),
349
+ sign({ sub: payload.sub, type: "refresh", jti: newJti }, opts.secret, {
350
+ expiresIn: refreshExpiresIn
351
+ })
352
+ ]);
353
+ if (cookieOpts !== void 0) {
354
+ const sameSite = cookieOpts.sameSite ?? "Lax";
355
+ const baseCookieOptions = {
356
+ httpOnly: cookieOpts.httpOnly ?? true,
357
+ sameSite,
358
+ secure: sameSite === "None" ? true : cookieOpts.secure ?? true,
359
+ path: cookieOpts.path ?? "/"
360
+ };
361
+ setCookie(c, accessTokenName, accessToken2, {
362
+ ...baseCookieOptions,
363
+ maxAge: expiresIn
364
+ });
365
+ setCookie(c, refreshTokenName, newRefreshToken, {
366
+ ...baseCookieOptions,
367
+ maxAge: refreshExpiresIn
368
+ });
369
+ return c.json({ ok: true }, 200);
370
+ }
371
+ return c.json({ accessToken: accessToken2, refreshToken: newRefreshToken }, 200);
372
+ }
373
+ const accessToken = await sign({ sub: payload.sub, type: "access" }, opts.secret, {
374
+ expiresIn
375
+ });
376
+ if (cookieOpts !== void 0) {
377
+ const sameSite = cookieOpts.sameSite ?? "Lax";
378
+ const baseCookieOptions = {
379
+ httpOnly: cookieOpts.httpOnly ?? true,
380
+ sameSite,
381
+ secure: sameSite === "None" ? true : cookieOpts.secure ?? true,
382
+ path: cookieOpts.path ?? "/"
383
+ };
384
+ setCookie(c, accessTokenName, accessToken, {
385
+ ...baseCookieOptions,
386
+ maxAge: expiresIn
387
+ });
388
+ return c.json({ ok: true }, 200);
389
+ }
390
+ return c.json({ accessToken }, 200);
391
+ };
392
+ },
393
+ middleware() {
394
+ return async (c, next) => {
395
+ const authHeader = c.req.header("Authorization");
396
+ if (!authHeader || !/^Bearer\s+/i.test(authHeader)) {
397
+ throw new HTTPException(401, { message: "Unauthorized" });
398
+ }
399
+ const token = authHeader.replace(/^Bearer\s+/i, "");
400
+ if (!token) {
401
+ throw new HTTPException(401, { message: "Unauthorized" });
402
+ }
403
+ let payload;
404
+ try {
405
+ payload = await verify(token, opts.secret);
406
+ } catch (err) {
407
+ console.warn("access token verify failed", err);
408
+ throw new HTTPException(401, { message: "Unauthorized" });
409
+ }
410
+ if (payload.type !== "access") {
411
+ throw new HTTPException(401, { message: "Unauthorized" });
412
+ }
413
+ c.set("user", payload);
414
+ await next();
415
+ };
416
+ }
417
+ };
418
+ }
419
+ function authMiddleware(opts) {
420
+ return async (c, next) => {
421
+ const authHeader = c.req.header("Authorization");
422
+ if (!authHeader || !/^Bearer\s+/i.test(authHeader)) {
423
+ throw new HTTPException(401, { message: "Unauthorized" });
424
+ }
425
+ const token = authHeader.replace(/^Bearer\s+/i, "");
426
+ if (!token) {
427
+ throw new HTTPException(401, { message: "Unauthorized" });
428
+ }
429
+ let payload;
430
+ try {
431
+ payload = await verify(token, opts.secret);
432
+ } catch (err) {
433
+ throw new HTTPException(401, { message: "Unauthorized", cause: err });
434
+ }
435
+ if (payload.type !== "access") {
436
+ throw new HTTPException(401, { message: "Unauthorized" });
437
+ }
438
+ c.set("user", payload);
439
+ await next();
440
+ };
441
+ }
442
+
443
+ export { authMiddleware, createAuth, kvBlacklistStore, pbkdf2Hasher, sign, verify };
444
+ //# sourceMappingURL=index.js.map
445
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/blacklist-stores/kv.ts","../src/hashers/pbkdf2.ts","../src/jwt.ts","../src/create-auth.ts","../src/middleware.ts"],"names":["utf8","toBase64url","fromBase64url","accessToken","HTTPException"],"mappings":"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","file":"index.js","sourcesContent":["import type { KVNamespace } from '@cloudflare/workers-types'\nimport type { BlacklistStore } from '../blacklist-store.js'\n\nasync function hashKey(jti: string): Promise<string> {\n const encoder = new TextEncoder()\n const buf = await crypto.subtle.digest('SHA-256', encoder.encode(jti))\n const bytes = new Uint8Array(buf)\n let hex = ''\n for (let i = 0; i < bytes.length; i++) {\n hex += (bytes[i] as number).toString(16).padStart(2, '0')\n }\n return hex\n}\n\nexport function kvBlacklistStore(kv: KVNamespace, opts?: { prefix?: string }): BlacklistStore {\n const prefix = opts?.prefix ?? 'nanoka-auth:bl:'\n\n return {\n async add(jti: string, expiresAt: number): Promise<void> {\n const ttl = Math.min(2592000, Math.max(60, expiresAt - Math.floor(Date.now() / 1000)))\n await kv.put(`${prefix}${await hashKey(jti)}`, '1', { expirationTtl: ttl })\n },\n\n async has(jti: string): Promise<boolean> {\n return (await kv.get(`${prefix}${await hashKey(jti)}`)) !== null\n },\n\n async addWithSubject(jti: string, sub: string, expiresAt: number): Promise<void> {\n const ttl = Math.min(2592000, Math.max(60, expiresAt - Math.floor(Date.now() / 1000)))\n await kv.put(`${prefix}${await hashKey(jti)}`, JSON.stringify({ sub }), {\n expirationTtl: ttl,\n })\n },\n\n async hasForSubject(jti: string, sub: string): Promise<boolean> {\n const value = await kv.get(`${prefix}${await hashKey(jti)}`)\n if (value === null) return false\n if (value === '1') return true\n try {\n const parsed = JSON.parse(value) as unknown\n if (typeof parsed === 'object' && parsed !== null && 'sub' in parsed) {\n return (parsed as { sub: unknown }).sub === sub\n }\n } catch {\n // malformed value: treat as blacklisted (conservative)\n }\n return true\n },\n }\n}\n","import type { Hasher } from '../hasher.js'\n\nconst ITERATIONS = 310_000\nconst SALT_BYTES = 16\nconst HASH_BYTES = 32\nconst HASH_NAME = 'SHA-256'\n\nfunction utf8(str: string): Uint8Array<ArrayBuffer> {\n return new TextEncoder().encode(str) as unknown as Uint8Array<ArrayBuffer>\n}\n\nfunction toBase64url(buf: ArrayBuffer): string {\n const bytes = new Uint8Array(buf)\n let binary = ''\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] as number)\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction fromBase64url(str: string): Uint8Array<ArrayBuffer> | null {\n try {\n const padded = str\n .replace(/-/g, '+')\n .replace(/_/g, '/')\n .padEnd(str.length + ((4 - (str.length % 4)) % 4), '=')\n const binary = atob(padded)\n const buf = new ArrayBuffer(binary.length)\n const bytes = new Uint8Array(buf)\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i)\n }\n return bytes\n } catch {\n return null\n }\n}\n\nasync function derive(\n plain: string,\n salt: Uint8Array<ArrayBuffer>,\n iterations: number,\n): Promise<ArrayBuffer> {\n const key = await crypto.subtle.importKey('raw', utf8(plain), 'PBKDF2', false, ['deriveBits'])\n return crypto.subtle.deriveBits(\n { name: 'PBKDF2', salt, iterations, hash: HASH_NAME },\n key,\n HASH_BYTES * 8,\n )\n}\n\nfunction timingSafeEqual(a: Uint8Array<ArrayBuffer>, b: Uint8Array<ArrayBuffer>): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) {\n diff |= (a[i] as number) ^ (b[i] as number)\n }\n return diff === 0\n}\n\nexport const pbkdf2Hasher: Hasher = {\n async hash(plain: string): Promise<string> {\n const saltBuf = new ArrayBuffer(SALT_BYTES)\n const salt = new Uint8Array(saltBuf) as Uint8Array<ArrayBuffer>\n crypto.getRandomValues(salt)\n const derived = await derive(plain, salt, ITERATIONS)\n return `$pbkdf2$${ITERATIONS}$${toBase64url(saltBuf)}$${toBase64url(derived)}`\n },\n\n async verify(plain: string, stored: string): Promise<boolean> {\n const parts = stored.split('$')\n if (parts.length !== 5 || parts[0] !== '') return false\n const algoId = parts[1]\n const iterStr = parts[2]\n const saltB64 = parts[3]\n const hashB64 = parts[4]\n if (algoId !== 'pbkdf2') return false\n if (iterStr === undefined || saltB64 === undefined || hashB64 === undefined) return false\n\n const iterations = Number.parseInt(iterStr, 10)\n if (!Number.isFinite(iterations) || iterations <= 0 || iterations > 1_000_000) return false\n\n const salt = fromBase64url(saltB64)\n const storedHash = fromBase64url(hashB64)\n\n if (salt === null || storedHash === null) return false\n if (salt.length === 0 || storedHash.length !== HASH_BYTES) return false\n\n const derived = await derive(plain, salt, iterations)\n const derivedBytes = new Uint8Array(derived) as Uint8Array<ArrayBuffer>\n\n return timingSafeEqual(derivedBytes, storedHash)\n },\n}\n","const ALGORITHM = 'HS256'\n\nexport interface SignOptions {\n expiresIn?: number\n}\n\nfunction utf8(str: string): Uint8Array<ArrayBuffer> {\n return new TextEncoder().encode(str) as unknown as Uint8Array<ArrayBuffer>\n}\n\nfunction toBase64url(buf: ArrayBuffer): string {\n const bytes = new Uint8Array(buf)\n let binary = ''\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i] as number)\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction fromBase64url(str: string): ArrayBuffer {\n const padded = str\n .replace(/-/g, '+')\n .replace(/_/g, '/')\n .padEnd(str.length + ((4 - (str.length % 4)) % 4), '=')\n const binary = atob(padded)\n const buf = new ArrayBuffer(binary.length)\n const bytes = new Uint8Array(buf)\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i)\n }\n return buf\n}\n\nasync function importKey(secret: string): Promise<CryptoKey> {\n return crypto.subtle.importKey('raw', utf8(secret), { name: 'HMAC', hash: 'SHA-256' }, false, [\n 'sign',\n 'verify',\n ])\n}\n\n/**\n * HS256 で JWT を署名する。\n * @remarks アルゴリズムは HS256 固定。RS256 / ES256 等の非対称アルゴリズムを追加する場合は\n * 鍵オブジェクト型を分離し、verify に期待する alg を引数で渡す API に変更すること(alg confusion 攻撃防止)。\n */\nexport async function sign(\n payload: Record<string, unknown>,\n secret: string,\n options?: SignOptions,\n): Promise<string> {\n if (!secret) throw new Error('secret must not be empty')\n const header = { alg: ALGORITHM, typ: 'JWT' }\n const headerB64 = toBase64url(utf8(JSON.stringify(header)).buffer as ArrayBuffer)\n\n if (options?.expiresIn !== undefined && !Number.isFinite(options.expiresIn)) {\n throw new Error('expiresIn must be a finite number')\n }\n const finalPayload =\n options?.expiresIn !== undefined\n ? { ...payload, exp: Math.floor(Date.now() / 1000) + options.expiresIn }\n : payload\n const payloadB64 = toBase64url(utf8(JSON.stringify(finalPayload)).buffer as ArrayBuffer)\n\n const signingInput = `${headerB64}.${payloadB64}`\n const key = await importKey(secret)\n const signatureBuf = await crypto.subtle.sign('HMAC', key, utf8(signingInput))\n const signatureB64 = toBase64url(signatureBuf)\n\n return `${signingInput}.${signatureB64}`\n}\n\n/**\n * HS256 JWT を検証してペイロードを返す。\n * @remarks 戻り値の型パラメータ T は実行時検証なし。受け取った payload は Zod 等で検証すること。\n * `nbf` / `iat` / `aud` / `iss` クレームは検証しない。必要な場合はアプリ側で payload を検証すること。\n */\nexport async function verify<T = Record<string, unknown>>(\n token: string,\n secret: string,\n): Promise<T> {\n if (!secret) throw new Error('secret must not be empty')\n const segments = token.split('.')\n if (segments.length !== 3) {\n throw new Error('Invalid token format')\n }\n\n const [headerB64, payloadB64, signatureB64] = segments as [string, string, string]\n\n const header = JSON.parse(new TextDecoder().decode(fromBase64url(headerB64))) as unknown\n if (typeof header !== 'object' || header === null || Array.isArray(header)) {\n throw new Error('Invalid token header')\n }\n if ((header as Record<string, unknown>).typ !== 'JWT') {\n throw new Error('Invalid token header')\n }\n const alg = (header as Record<string, unknown>).alg\n if (typeof alg !== 'string' || alg.toLowerCase() === 'none') {\n throw new Error('Invalid token header')\n }\n if (alg !== ALGORITHM) {\n throw new Error('Invalid token header')\n }\n\n const signingInput = `${headerB64}.${payloadB64}`\n const key = await importKey(secret)\n const signatureBuf = fromBase64url(signatureB64)\n const valid = await crypto.subtle.verify('HMAC', key, signatureBuf, utf8(signingInput))\n if (!valid) {\n throw new Error('Invalid signature')\n }\n\n const payload = JSON.parse(new TextDecoder().decode(fromBase64url(payloadB64))) as unknown\n if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {\n throw new Error('Invalid payload')\n }\n const payloadObj = payload as Record<string, unknown>\n\n if (payloadObj.exp !== undefined) {\n if (typeof payloadObj.exp !== 'number' || !Number.isFinite(payloadObj.exp)) {\n throw new Error('Invalid exp claim')\n }\n if (Math.floor(Date.now() / 1000) >= payloadObj.exp) {\n throw new Error('Token expired')\n }\n }\n\n return payloadObj as T\n}\n","import type { NanokaModel } from '@nanokajs/core'\nimport type { Handler, MiddlewareHandler } from 'hono'\nimport { getCookie, setCookie } from 'hono/cookie'\nimport { HTTPException } from 'hono/http-exception'\nimport type { BlacklistStore } from './blacklist-store.js'\nimport type { Hasher } from './hasher.js'\nimport { pbkdf2Hasher } from './hashers/pbkdf2.js'\nimport { sign, verify } from './jwt.js'\n\nexport interface CookieOptions {\n httpOnly?: boolean\n sameSite?: 'Strict' | 'Lax' | 'None'\n secure?: boolean\n path?: string\n accessTokenName?: string\n refreshTokenName?: string\n}\n\nexport interface CreateAuthOptions {\n // biome-ignore lint/suspicious/noExplicitAny: any is necessary for NanokaModel generic constraint\n model: NanokaModel<any>\n secret: string\n fields: { identifier: string; password: string }\n hasher?: Hasher\n jwt?: { expiresIn?: number; refreshExpiresIn?: number; rotation?: boolean }\n cookie?: CookieOptions\n blacklist?: BlacklistStore\n}\n\nexport interface AuthInstance {\n loginHandler(): Handler\n refreshHandler(): Handler\n middleware(): MiddlewareHandler\n}\n\nexport function createAuth(opts: CreateAuthOptions): AuthInstance {\n if (opts.secret.length < 32) {\n throw new Error('createAuth: secret must be at least 32 characters')\n }\n\n const rotation = opts.jwt?.rotation ?? false\n if (rotation && opts.blacklist === undefined) {\n throw new Error('createAuth: blacklist is required when jwt.rotation is true')\n }\n\n const hasher = opts.hasher ?? pbkdf2Hasher\n const expiresIn = opts.jwt?.expiresIn ?? 900\n const refreshExpiresIn = opts.jwt?.refreshExpiresIn ?? 604_800\n const identifierField = opts.fields.identifier\n const passwordField = opts.fields.password\n const dummyHashPromise = hasher.hash('__dummy__')\n\n const cookieOpts = opts.cookie\n const accessTokenName = cookieOpts?.accessTokenName ?? 'access_token'\n const refreshTokenName = cookieOpts?.refreshTokenName ?? 'refresh_token'\n\n return {\n loginHandler(): Handler {\n return async (c) => {\n let body: Record<string, unknown>\n try {\n body = await c.req.json<Record<string, unknown>>()\n } catch {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n const identifierValue = body[identifierField]\n const passwordValue = body[passwordField]\n\n if (typeof identifierValue !== 'string' || typeof passwordValue !== 'string') {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: model is typed as NanokaModel<any> per CreateAuthOptions\n const user = await opts.model.findOne({ [identifierField]: identifierValue } as any)\n\n if (user === null) {\n const dummyHash = await dummyHashPromise\n await hasher.verify('__dummy__', dummyHash)\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: user shape is unknown at compile time\n const storedHash = (user as any)[passwordField] as string\n const valid = await hasher.verify(passwordValue, storedHash)\n\n if (!valid) {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n\n // biome-ignore lint/suspicious/noExplicitAny: user shape is unknown at compile time\n const sub: string = String((user as any).id ?? (user as any)[identifierField])\n\n const refreshPayload: Record<string, unknown> = { sub, type: 'refresh' }\n if (rotation) {\n refreshPayload.jti = crypto.randomUUID()\n }\n\n const [accessToken, refreshToken] = await Promise.all([\n sign({ sub, type: 'access' }, opts.secret, { expiresIn }),\n sign(refreshPayload, opts.secret, { expiresIn: refreshExpiresIn }),\n ])\n\n if (cookieOpts !== undefined) {\n const sameSite = cookieOpts.sameSite ?? 'Lax'\n const baseCookieOptions = {\n httpOnly: cookieOpts.httpOnly ?? true,\n sameSite,\n secure: sameSite === 'None' ? true : (cookieOpts.secure ?? true),\n path: cookieOpts.path ?? '/',\n } as const\n setCookie(c, accessTokenName, accessToken, {\n ...baseCookieOptions,\n maxAge: expiresIn,\n })\n setCookie(c, refreshTokenName, refreshToken, {\n ...baseCookieOptions,\n maxAge: refreshExpiresIn,\n })\n return c.json({ ok: true }, 200)\n }\n\n return c.json({ accessToken, refreshToken }, 200)\n }\n },\n\n refreshHandler(): Handler {\n return async (c) => {\n let refreshToken: string | undefined\n\n if (cookieOpts !== undefined) {\n refreshToken = getCookie(c, refreshTokenName)\n if ((refreshToken === undefined || refreshToken === '') && rotation) {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n }\n\n if (refreshToken === undefined || refreshToken === '') {\n let body: Record<string, unknown>\n try {\n body = await c.req.json<Record<string, unknown>>()\n } catch {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n const bodyRefreshToken = body.refreshToken\n if (typeof bodyRefreshToken !== 'string') {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n refreshToken = bodyRefreshToken\n }\n\n let payload: Record<string, unknown>\n try {\n payload = await verify<Record<string, unknown>>(refreshToken, opts.secret)\n } catch (err) {\n console.warn('refresh token verify failed', err)\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n\n if (payload.type !== 'refresh') {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n\n if (typeof payload.sub !== 'string' || payload.sub.length === 0) {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n\n if (rotation) {\n const jti = payload.jti\n if (typeof jti !== 'string' || jti.length === 0) {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n if (jti.length > 256) {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n if (!/^[A-Za-z0-9_-]+$/.test(jti)) {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n // biome-ignore lint/style/noNonNullAssertion: blacklist is guaranteed non-null when rotation is true (validated at createAuth call)\n const blacklist = opts.blacklist!\n // Use subject-aware methods only when BOTH are present.\n // Using them independently (e.g. hasForSubject without addWithSubject) would\n // cause the read and write paths to use different storage keys, allowing\n // a revoked JTI to be accepted on the read path (replay attack).\n const useSubjectAware =\n blacklist.hasForSubject !== undefined && blacklist.addWithSubject !== undefined\n const blacklisted = useSubjectAware\n ? await blacklist.hasForSubject!(jti, payload.sub)\n : await blacklist.has(jti)\n if (blacklisted) {\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n const exp =\n typeof payload.exp === 'number'\n ? payload.exp\n : Math.floor(Date.now() / 1000) + refreshExpiresIn\n try {\n if (useSubjectAware) {\n await blacklist.addWithSubject!(jti, payload.sub, exp)\n } else {\n await blacklist.add(jti, exp)\n }\n } catch (err) {\n console.error('blacklist.add failed', err)\n throw new HTTPException(401, { message: 'Invalid credentials' })\n }\n\n const newJti = crypto.randomUUID()\n const [accessToken, newRefreshToken] = await Promise.all([\n sign({ sub: payload.sub, type: 'access' }, opts.secret, { expiresIn }),\n sign({ sub: payload.sub, type: 'refresh', jti: newJti }, opts.secret, {\n expiresIn: refreshExpiresIn,\n }),\n ])\n\n if (cookieOpts !== undefined) {\n const sameSite = cookieOpts.sameSite ?? 'Lax'\n const baseCookieOptions = {\n httpOnly: cookieOpts.httpOnly ?? true,\n sameSite,\n secure: sameSite === 'None' ? true : (cookieOpts.secure ?? true),\n path: cookieOpts.path ?? '/',\n } as const\n setCookie(c, accessTokenName, accessToken, {\n ...baseCookieOptions,\n maxAge: expiresIn,\n })\n setCookie(c, refreshTokenName, newRefreshToken, {\n ...baseCookieOptions,\n maxAge: refreshExpiresIn,\n })\n return c.json({ ok: true }, 200)\n }\n\n return c.json({ accessToken, refreshToken: newRefreshToken }, 200)\n }\n\n const accessToken = await sign({ sub: payload.sub, type: 'access' }, opts.secret, {\n expiresIn,\n })\n\n if (cookieOpts !== undefined) {\n const sameSite = cookieOpts.sameSite ?? 'Lax'\n const baseCookieOptions = {\n httpOnly: cookieOpts.httpOnly ?? true,\n sameSite,\n secure: sameSite === 'None' ? true : (cookieOpts.secure ?? true),\n path: cookieOpts.path ?? '/',\n } as const\n setCookie(c, accessTokenName, accessToken, {\n ...baseCookieOptions,\n maxAge: expiresIn,\n })\n return c.json({ ok: true }, 200)\n }\n\n return c.json({ accessToken }, 200)\n }\n },\n\n middleware(): MiddlewareHandler {\n return async (c, next) => {\n const authHeader = c.req.header('Authorization')\n if (!authHeader || !/^Bearer\\s+/i.test(authHeader)) {\n throw new HTTPException(401, { message: 'Unauthorized' })\n }\n const token = authHeader.replace(/^Bearer\\s+/i, '')\n if (!token) {\n throw new HTTPException(401, { message: 'Unauthorized' })\n }\n let payload: Record<string, unknown>\n try {\n payload = await verify<Record<string, unknown>>(token, opts.secret)\n } catch (err) {\n console.warn('access token verify failed', err)\n throw new HTTPException(401, { message: 'Unauthorized' })\n }\n if (payload.type !== 'access') {\n throw new HTTPException(401, { message: 'Unauthorized' })\n }\n // biome-ignore lint/suspicious/noExplicitAny: Variables shape is unknown at this call site\n c.set('user' as any, payload)\n await next()\n }\n },\n }\n}\n","import type { MiddlewareHandler } from 'hono'\nimport { HTTPException } from 'hono/http-exception'\nimport { verify } from './jwt.js'\n\nexport function authMiddleware<\n TPayload extends Record<string, unknown> = Record<string, unknown>,\n>(opts: { secret: string }): MiddlewareHandler<{ Variables: { user: TPayload } }> {\n return async (c, next) => {\n const authHeader = c.req.header('Authorization')\n if (!authHeader || !/^Bearer\\s+/i.test(authHeader)) {\n throw new HTTPException(401, { message: 'Unauthorized' })\n }\n const token = authHeader.replace(/^Bearer\\s+/i, '')\n if (!token) {\n throw new HTTPException(401, { message: 'Unauthorized' })\n }\n let payload: TPayload\n try {\n payload = await verify<TPayload>(token, opts.secret)\n } catch (err) {\n throw new HTTPException(401, { message: 'Unauthorized', cause: err })\n }\n if ((payload as Record<string, unknown>).type !== 'access') {\n throw new HTTPException(401, { message: 'Unauthorized' })\n }\n c.set('user', payload)\n await next()\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@nanokajs/auth",
3
+ "version": "1.6.0",
4
+ "type": "module",
5
+ "description": "Authentication utilities for Nanoka on Cloudflare Workers.",
6
+ "author": "Eiichiro Iriguchi <eiichiro_iriguchi@a-1ro.dev>",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/nanokajs/nanoka.git"
11
+ },
12
+ "homepage": "https://github.com/nanokajs/nanoka#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/nanokajs/nanoka/issues"
15
+ },
16
+ "keywords": [
17
+ "nanoka",
18
+ "auth",
19
+ "cloudflare-workers",
20
+ "hono",
21
+ "d1"
22
+ ],
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
26
+ "sideEffects": false,
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ }
32
+ },
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "files": [
36
+ "dist",
37
+ "LICENSE",
38
+ "CHANGELOG.md"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "test": "vitest run -c vitest.config.ts && vitest run -c vitest.node.config.ts",
46
+ "test:workers": "vitest run -c vitest.config.ts",
47
+ "test:node": "vitest run -c vitest.node.config.ts",
48
+ "typecheck": "tsc --noEmit",
49
+ "prepublishOnly": "pnpm build && pnpm test && pnpm typecheck"
50
+ },
51
+ "peerDependencies": {
52
+ "@nanokajs/core": "^1.0.0",
53
+ "hono": "^4.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@nanokajs/core": "workspace:*",
57
+ "@cloudflare/vitest-pool-workers": "^0.5.0",
58
+ "hono": "^4.0.0",
59
+ "@cloudflare/workers-types": "^4.20240925.0",
60
+ "@types/node": "^25.6.0",
61
+ "tsup": "^8.3.0",
62
+ "typescript": "^5.6.0",
63
+ "vitest": "^2.1.0",
64
+ "wrangler": "^3.80.0"
65
+ }
66
+ }