@authdog/fastify 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/README.md +85 -0
- package/dist/index.d.mts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @authdog/fastify
|
|
2
|
+
|
|
3
|
+
**Authdog SDK for [Fastify](https://fastify.dev).**
|
|
4
|
+
|
|
5
|
+
A tiny, high-performance plugin that validates Authdog sessions on every
|
|
6
|
+
request and gives you an idiomatic `request.authdog` context plus a
|
|
7
|
+
`requireAuth` guard — built on [`@authdog/node-commons`](../node-commons).
|
|
8
|
+
|
|
9
|
+
- 🔌 **Drop-in plugin** — `app.register(authdogPlugin, { publicKey })`.
|
|
10
|
+
- 🔐 **Secure by default** — public key (and its identity host) validated once
|
|
11
|
+
at registration; tokens only trusted after the identity host confirms them.
|
|
12
|
+
- 🧱 **No assumptions** — parses cookies itself; `@fastify/cookie` not required.
|
|
13
|
+
- 🟦 **Typed** — `request.authdog` and `app.authdog` are fully typed via module
|
|
14
|
+
augmentation.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bun add @authdog/fastify fastify
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Set your Authdog public key (safe to expose):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
AUTHDOG_PK=pk_xxxxxxxxxxxxxxxx
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import Fastify from "fastify";
|
|
32
|
+
import { authdogPlugin } from "@authdog/fastify";
|
|
33
|
+
|
|
34
|
+
const app = Fastify();
|
|
35
|
+
|
|
36
|
+
await app.register(authdogPlugin, {
|
|
37
|
+
publicKey: process.env.AUTHDOG_PK!,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Every request now carries `request.authdog` ({ token, user, isAuthenticated }).
|
|
41
|
+
app.get("/", async (request) => {
|
|
42
|
+
return request.authdog?.isAuthenticated
|
|
43
|
+
? `Hello ${JSON.stringify(request.authdog.user)}`
|
|
44
|
+
: "Not signed in";
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Protect a route with the built-in guard (the real enforcement point).
|
|
48
|
+
app.get(
|
|
49
|
+
"/me",
|
|
50
|
+
{ preHandler: app.authdog.requireAuth },
|
|
51
|
+
async (request) => request.authdog!.user,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Logout: clears the session cookie and redirects to a sanitized ?redirect_uri.
|
|
55
|
+
app.get("/logout", (request, reply) => app.authdog.logout(request, reply));
|
|
56
|
+
|
|
57
|
+
await app.listen({ port: 3000 });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Token resolution
|
|
61
|
+
|
|
62
|
+
On each request the plugin looks for a token in this order:
|
|
63
|
+
|
|
64
|
+
1. The `authdog-session` cookie.
|
|
65
|
+
2. An `Authorization: Bearer <token>` header.
|
|
66
|
+
|
|
67
|
+
If a token is found it is verified against the identity host's `userinfo`
|
|
68
|
+
endpoint and, on success, `request.authdog.isAuthenticated` becomes `true` and
|
|
69
|
+
`request.authdog.user` is populated. A missing or invalid token never throws —
|
|
70
|
+
it simply yields an unauthenticated context.
|
|
71
|
+
|
|
72
|
+
### Options
|
|
73
|
+
|
|
74
|
+
| Option | Type | Default | Description |
|
|
75
|
+
| --------------- | --------- | ------- | --------------------------------------------------------------------------- |
|
|
76
|
+
| `publicKey` | `string` | — | Authdog public key (`pk_…`). Required. |
|
|
77
|
+
| `secretKey` | `string` | — | Reserved for future server-side session revocation. Currently unused. |
|
|
78
|
+
| `fetchUserInfo` | `boolean` | `true` | When `false`, skips the per-request `userinfo` call (token is not verified). |
|
|
79
|
+
|
|
80
|
+
> ⚠️ `request.authdog` is informational. Always gate protected routes with
|
|
81
|
+
> `app.authdog.requireAuth` (or your own check on `isAuthenticated`).
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { preHandlerHookHandler, FastifyRequest, FastifyReply, FastifyPluginCallback } from 'fastify';
|
|
2
|
+
|
|
3
|
+
/** Options accepted by {@link authdogPlugin} when registered. */
|
|
4
|
+
interface AuthdogPluginOptions {
|
|
5
|
+
/** Authdog public key (`pk_…`). Validated once at registration. */
|
|
6
|
+
publicKey: string;
|
|
7
|
+
/**
|
|
8
|
+
* Secret key. Reserved for future server-side session revocation; not yet
|
|
9
|
+
* used by the plugin.
|
|
10
|
+
*/
|
|
11
|
+
secretKey?: string;
|
|
12
|
+
/**
|
|
13
|
+
* When `false`, the plugin will not call the identity host's `userinfo`
|
|
14
|
+
* endpoint on every request. The token is still exposed on
|
|
15
|
+
* `request.authdog.token`, but `isAuthenticated` stays `false` because the
|
|
16
|
+
* token has not been verified. Defaults to `true`.
|
|
17
|
+
*/
|
|
18
|
+
fetchUserInfo?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Per-request authentication context attached to `request.authdog` by the
|
|
22
|
+
* `onRequest` hook. `isAuthenticated` is only ever `true` when a token was
|
|
23
|
+
* present AND the identity host confirmed it (`meta.code === 200`).
|
|
24
|
+
*/
|
|
25
|
+
interface AuthdogRequestContext {
|
|
26
|
+
token: string | null;
|
|
27
|
+
user: unknown | null;
|
|
28
|
+
isAuthenticated: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** API decorated onto the Fastify instance as `fastify.authdog`. */
|
|
31
|
+
interface AuthdogInstanceApi {
|
|
32
|
+
/**
|
|
33
|
+
* Route-level `preHandler` that replies `401` unless the request is
|
|
34
|
+
* authenticated. This is the real enforcement point — `request.authdog`
|
|
35
|
+
* alone is informational.
|
|
36
|
+
*/
|
|
37
|
+
requireAuth: preHandlerHookHandler;
|
|
38
|
+
/** Returns the validated public-key payload as a JSON string. */
|
|
39
|
+
getPublicKey: () => string;
|
|
40
|
+
/**
|
|
41
|
+
* Clears the session cookie and redirects to a sanitized `redirect_uri`
|
|
42
|
+
* query parameter (open-redirect safe), defaulting to `/`.
|
|
43
|
+
*/
|
|
44
|
+
logout: (request: FastifyRequest, reply: FastifyReply) => void;
|
|
45
|
+
}
|
|
46
|
+
declare module "fastify" {
|
|
47
|
+
interface FastifyRequest {
|
|
48
|
+
authdog?: AuthdogRequestContext;
|
|
49
|
+
}
|
|
50
|
+
interface FastifyInstance {
|
|
51
|
+
authdog: AuthdogInstanceApi;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Authdog Fastify plugin. Register it once on your instance:
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* await app.register(authdogPlugin, { publicKey: process.env.AUTHDOG_PK! });
|
|
60
|
+
* app.get("/me", { preHandler: app.authdog.requireAuth }, async (req) => req.authdog!.user);
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* Wrapped with `fastify-plugin` so the request/instance decorations are not
|
|
64
|
+
* encapsulated and are visible to sibling plugins and routes.
|
|
65
|
+
*/
|
|
66
|
+
declare const authdogPlugin: FastifyPluginCallback<AuthdogPluginOptions>;
|
|
67
|
+
|
|
68
|
+
export { type AuthdogInstanceApi, type AuthdogPluginOptions, type AuthdogRequestContext, authdogPlugin, authdogPlugin as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { preHandlerHookHandler, FastifyRequest, FastifyReply, FastifyPluginCallback } from 'fastify';
|
|
2
|
+
|
|
3
|
+
/** Options accepted by {@link authdogPlugin} when registered. */
|
|
4
|
+
interface AuthdogPluginOptions {
|
|
5
|
+
/** Authdog public key (`pk_…`). Validated once at registration. */
|
|
6
|
+
publicKey: string;
|
|
7
|
+
/**
|
|
8
|
+
* Secret key. Reserved for future server-side session revocation; not yet
|
|
9
|
+
* used by the plugin.
|
|
10
|
+
*/
|
|
11
|
+
secretKey?: string;
|
|
12
|
+
/**
|
|
13
|
+
* When `false`, the plugin will not call the identity host's `userinfo`
|
|
14
|
+
* endpoint on every request. The token is still exposed on
|
|
15
|
+
* `request.authdog.token`, but `isAuthenticated` stays `false` because the
|
|
16
|
+
* token has not been verified. Defaults to `true`.
|
|
17
|
+
*/
|
|
18
|
+
fetchUserInfo?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Per-request authentication context attached to `request.authdog` by the
|
|
22
|
+
* `onRequest` hook. `isAuthenticated` is only ever `true` when a token was
|
|
23
|
+
* present AND the identity host confirmed it (`meta.code === 200`).
|
|
24
|
+
*/
|
|
25
|
+
interface AuthdogRequestContext {
|
|
26
|
+
token: string | null;
|
|
27
|
+
user: unknown | null;
|
|
28
|
+
isAuthenticated: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** API decorated onto the Fastify instance as `fastify.authdog`. */
|
|
31
|
+
interface AuthdogInstanceApi {
|
|
32
|
+
/**
|
|
33
|
+
* Route-level `preHandler` that replies `401` unless the request is
|
|
34
|
+
* authenticated. This is the real enforcement point — `request.authdog`
|
|
35
|
+
* alone is informational.
|
|
36
|
+
*/
|
|
37
|
+
requireAuth: preHandlerHookHandler;
|
|
38
|
+
/** Returns the validated public-key payload as a JSON string. */
|
|
39
|
+
getPublicKey: () => string;
|
|
40
|
+
/**
|
|
41
|
+
* Clears the session cookie and redirects to a sanitized `redirect_uri`
|
|
42
|
+
* query parameter (open-redirect safe), defaulting to `/`.
|
|
43
|
+
*/
|
|
44
|
+
logout: (request: FastifyRequest, reply: FastifyReply) => void;
|
|
45
|
+
}
|
|
46
|
+
declare module "fastify" {
|
|
47
|
+
interface FastifyRequest {
|
|
48
|
+
authdog?: AuthdogRequestContext;
|
|
49
|
+
}
|
|
50
|
+
interface FastifyInstance {
|
|
51
|
+
authdog: AuthdogInstanceApi;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Authdog Fastify plugin. Register it once on your instance:
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* await app.register(authdogPlugin, { publicKey: process.env.AUTHDOG_PK! });
|
|
60
|
+
* app.get("/me", { preHandler: app.authdog.requireAuth }, async (req) => req.authdog!.user);
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* Wrapped with `fastify-plugin` so the request/instance decorations are not
|
|
64
|
+
* encapsulated and are visible to sibling plugins and routes.
|
|
65
|
+
*/
|
|
66
|
+
declare const authdogPlugin: FastifyPluginCallback<AuthdogPluginOptions>;
|
|
67
|
+
|
|
68
|
+
export { type AuthdogInstanceApi, type AuthdogPluginOptions, type AuthdogRequestContext, authdogPlugin, authdogPlugin as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var P=Object.create;var c=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var C=Object.getPrototypeOf,H=Object.prototype.hasOwnProperty;var O=(t,e)=>{for(var o in e)c(t,o,{get:e[o],enumerable:!0})},g=(t,e,o,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of q(e))!H.call(t,r)&&r!==o&&c(t,r,{get:()=>e[r],enumerable:!(a=R(e,r))||a.enumerable});return t};var I=(t,e,o)=>(o=t!=null?P(C(t)):{},g(e||!t||!t.__esModule?c(o,"default",{value:t,enumerable:!0}):o,t)),S=t=>g(c({},"__esModule",{value:!0}),t);var E={};O(E,{authdogPlugin:()=>l,default:()=>y});module.exports=S(E);var f=I(require("fastify-plugin")),n=require("@authdog/node-commons"),p="authdog-session",F=t=>{let e=(0,n.parseCookies)(t.headers.cookie??null).find(a=>a.name===p)?.value;if(e)return e;let o=t.headers.authorization;if(o&&o.startsWith("Bearer ")){let a=o.slice(7).trim();return a.length>0?a:null}return null},b=(t,e,o)=>{let{publicKey:a,fetchUserInfo:r}=e,h=(0,n.validateAndParsePublicKey)(a),{identityHost:A,environmentId:m}=h;t.decorateRequest("authdog",void 0),t.addHook("onRequest",async u=>{let i=F(u),s={token:i,user:null,isAuthenticated:!1};if(i&&r!==!1)try{let d=await(0,n.fetchUserData)(A,m,i);(0,n.isAuthenticatedUserInfo)(d)&&(s.user=d.user??null,s.isAuthenticated=!0)}catch{}u.authdog=s});let k={requireAuth:(u,i,s)=>{if(!u.authdog?.isAuthenticated){i.code(401).send({error:"Unauthorized"});return}s()},getPublicKey:()=>JSON.stringify(h),logout:(u,i)=>{let s=process.env.NODE_ENV==="production"?" Secure;":"";i.header("Set-Cookie",`${p}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${s} SameSite=Lax`);let d=u.query??{},x=(0,n.sanitizeRedirectPath)(d.redirect_uri,"/");i.redirect(x)}};t.decorate("authdog",k),o()},l=(0,f.default)(b,{name:"@authdog/fastify",fastify:"4.x || 5.x"}),y=l;0&&(module.exports={authdogPlugin});
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/plugin.ts"],"sourcesContent":["// Re-export the type-only module augmentation of \"fastify\" so consumers get\n// typed `request.authdog` / `fastify.authdog` simply by importing this package.\nimport \"./types\";\n\nexport { authdogPlugin, default } from \"./plugin\";\nexport type {\n AuthdogInstanceApi,\n AuthdogPluginOptions,\n AuthdogRequestContext,\n} from \"./types\";\n","import fp from \"fastify-plugin\";\nimport type {\n FastifyPluginCallback,\n FastifyReply,\n FastifyRequest,\n preHandlerHookHandler,\n} from \"fastify\";\nimport {\n fetchUserData,\n isAuthenticatedUserInfo,\n parseCookies,\n sanitizeRedirectPath,\n validateAndParsePublicKey,\n} from \"@authdog/node-commons\";\nimport type {\n AuthdogInstanceApi,\n AuthdogPluginOptions,\n AuthdogRequestContext,\n} from \"./types\";\n\n/** Name of the cookie carrying the session token (shared across SDKs). */\nconst SESSION_COOKIE_NAME = \"authdog-session\";\n\n/**\n * Extracts the session token, preferring the `authdog-session` cookie and\n * falling back to a `Bearer` authorization header.\n *\n * The cookie header is parsed with the shared {@link parseCookies} (splits on\n * the first `=`, URL-decodes) so JWT values containing `=` are not truncated.\n * We deliberately do NOT depend on `@fastify/cookie` being registered.\n */\nconst extractToken = (request: FastifyRequest): string | null => {\n const fromCookie = parseCookies(request.headers.cookie ?? null).find(\n (c) => c.name === SESSION_COOKIE_NAME,\n )?.value;\n if (fromCookie) {\n return fromCookie;\n }\n\n const authz = request.headers.authorization;\n if (authz && authz.startsWith(\"Bearer \")) {\n const token = authz.slice(\"Bearer \".length).trim();\n return token.length > 0 ? token : null;\n }\n\n return null;\n};\n\nconst authdogPluginCallback: FastifyPluginCallback<AuthdogPluginOptions> = (\n fastify,\n options,\n done,\n) => {\n const { publicKey, fetchUserInfo } = options;\n\n // Validate and parse the public key once at registration. This enforces the\n // trusted identity-host allowlist (SSRF / token-exfiltration protection) and\n // fails fast if the key is malformed, rather than per-request.\n const payload = validateAndParsePublicKey(publicKey);\n const { identityHost, environmentId } = payload;\n\n // Stable per-request default. Reassigned to a fresh object in the hook below,\n // so the (deprecated in v5) shared-reference pitfall does not apply.\n fastify.decorateRequest(\"authdog\", undefined);\n\n fastify.addHook(\"onRequest\", async (request) => {\n const token = extractToken(request);\n\n const context: AuthdogRequestContext = {\n token,\n user: null,\n isAuthenticated: false,\n };\n\n if (token && fetchUserInfo !== false) {\n try {\n const data = await fetchUserData(identityHost, environmentId, token);\n // Only trust a genuine success envelope (`meta.code === 200` + a user);\n // a 200 HTTP status alone is not sufficient.\n if (isAuthenticatedUserInfo(data)) {\n context.user = data.user ?? null;\n context.isAuthenticated = true;\n }\n } catch {\n // Never throw from the hook: an invalid/expired token simply yields an\n // unauthenticated context. Enforcement happens in `requireAuth`.\n }\n }\n\n request.authdog = context;\n });\n\n const requireAuth: preHandlerHookHandler = (request, reply, next) => {\n if (!request.authdog?.isAuthenticated) {\n reply.code(401).send({ error: \"Unauthorized\" });\n return;\n }\n next();\n };\n\n const logout = (request: FastifyRequest, reply: FastifyReply): void => {\n // Expire the session cookie. `Secure` is gated on production so local HTTP\n // development still clears the cookie; HttpOnly + SameSite=Lax retained.\n const secure = process.env.NODE_ENV === \"production\" ? \" Secure;\" : \"\";\n reply.header(\n \"Set-Cookie\",\n `${SESSION_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${secure} SameSite=Lax`,\n );\n\n // Sanitize the redirect target to prevent an open redirect via an\n // attacker-controlled `redirect_uri` query parameter.\n const query = (request.query ?? {}) as Record<string, unknown>;\n const target = sanitizeRedirectPath(query.redirect_uri, \"/\");\n reply.redirect(target);\n };\n\n const api: AuthdogInstanceApi = {\n requireAuth,\n getPublicKey: () => JSON.stringify(payload),\n logout,\n };\n\n fastify.decorate(\"authdog\", api);\n\n done();\n};\n\n/**\n * Authdog Fastify plugin. Register it once on your instance:\n *\n * ```ts\n * await app.register(authdogPlugin, { publicKey: process.env.AUTHDOG_PK! });\n * app.get(\"/me\", { preHandler: app.authdog.requireAuth }, async (req) => req.authdog!.user);\n * ```\n *\n * Wrapped with `fastify-plugin` so the request/instance decorations are not\n * encapsulated and are visible to sibling plugins and routes.\n */\nexport const authdogPlugin = fp(authdogPluginCallback, {\n name: \"@authdog/fastify\",\n fastify: \"4.x || 5.x\",\n});\n\nexport default authdogPlugin;\n"],"mappings":"0jBAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,mBAAAE,EAAA,YAAAC,IAAA,eAAAC,EAAAJ,GCAA,IAAAK,EAAe,6BAOfC,EAMO,iCAQDC,EAAsB,kBAUtBC,EAAgBC,GAA2C,CAC/D,IAAMC,KAAa,gBAAaD,EAAQ,QAAQ,QAAU,IAAI,EAAE,KAC7DE,GAAMA,EAAE,OAASJ,CACpB,GAAG,MACH,GAAIG,EACF,OAAOA,EAGT,IAAME,EAAQH,EAAQ,QAAQ,cAC9B,GAAIG,GAASA,EAAM,WAAW,SAAS,EAAG,CACxC,IAAMC,EAAQD,EAAM,MAAM,CAAgB,EAAE,KAAK,EACjD,OAAOC,EAAM,OAAS,EAAIA,EAAQ,IACpC,CAEA,OAAO,IACT,EAEMC,EAAqE,CACzEC,EACAC,EACAC,IACG,CACH,GAAM,CAAE,UAAAC,EAAW,cAAAC,CAAc,EAAIH,EAK/BI,KAAU,6BAA0BF,CAAS,EAC7C,CAAE,aAAAG,EAAc,cAAAC,CAAc,EAAIF,EAIxCL,EAAQ,gBAAgB,UAAW,MAAS,EAE5CA,EAAQ,QAAQ,YAAa,MAAON,GAAY,CAC9C,IAAMI,EAAQL,EAAaC,CAAO,EAE5Bc,EAAiC,CACrC,MAAAV,EACA,KAAM,KACN,gBAAiB,EACnB,EAEA,GAAIA,GAASM,IAAkB,GAC7B,GAAI,CACF,IAAMK,EAAO,QAAM,iBAAcH,EAAcC,EAAeT,CAAK,KAG/D,2BAAwBW,CAAI,IAC9BD,EAAQ,KAAOC,EAAK,MAAQ,KAC5BD,EAAQ,gBAAkB,GAE9B,MAAQ,CAGR,CAGFd,EAAQ,QAAUc,CACpB,CAAC,EA0BD,IAAME,EAA0B,CAC9B,YAzByC,CAAChB,EAASiB,EAAOC,IAAS,CACnE,GAAI,CAAClB,EAAQ,SAAS,gBAAiB,CACrCiB,EAAM,KAAK,GAAG,EAAE,KAAK,CAAE,MAAO,cAAe,CAAC,EAC9C,MACF,CACAC,EAAK,CACP,EAoBE,aAAc,IAAM,KAAK,UAAUP,CAAO,EAC1C,OAnBa,CAACX,EAAyBiB,IAA8B,CAGrE,IAAME,EAAS,QAAQ,IAAI,WAAa,aAAe,WAAa,GACpEF,EAAM,OACJ,aACA,GAAGnB,CAAmB,8DAA8DqB,CAAM,eAC5F,EAIA,IAAMC,EAASpB,EAAQ,OAAS,CAAC,EAC3BqB,KAAS,wBAAqBD,EAAM,aAAc,GAAG,EAC3DH,EAAM,SAASI,CAAM,CACvB,CAMA,EAEAf,EAAQ,SAAS,UAAWU,CAAG,EAE/BR,EAAK,CACP,EAaac,KAAgB,EAAAC,SAAGlB,EAAuB,CACrD,KAAM,mBACN,QAAS,YACX,CAAC,EAEMmB,EAAQF","names":["index_exports","__export","authdogPlugin","plugin_default","__toCommonJS","import_fastify_plugin","import_node_commons","SESSION_COOKIE_NAME","extractToken","request","fromCookie","c","authz","token","authdogPluginCallback","fastify","options","done","publicKey","fetchUserInfo","payload","identityHost","environmentId","context","data","api","reply","next","secure","query","target","authdogPlugin","fp","plugin_default"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import y from"fastify-plugin";import{fetchUserData as A,isAuthenticatedUserInfo as m,parseCookies as k,sanitizeRedirectPath as x,validateAndParsePublicKey as P}from"@authdog/node-commons";var d="authdog-session",R=o=>{let r=k(o.headers.cookie??null).find(n=>n.name===d)?.value;if(r)return r;let i=o.headers.authorization;if(i&&i.startsWith("Bearer ")){let n=i.slice(7).trim();return n.length>0?n:null}return null},q=(o,r,i)=>{let{publicKey:n,fetchUserInfo:l}=r,u=P(n),{identityHost:h,environmentId:g}=u;o.decorateRequest("authdog",void 0),o.addHook("onRequest",async a=>{let t=R(a),e={token:t,user:null,isAuthenticated:!1};if(t&&l!==!1)try{let s=await A(h,g,t);m(s)&&(e.user=s.user??null,e.isAuthenticated=!0)}catch{}a.authdog=e});let f={requireAuth:(a,t,e)=>{if(!a.authdog?.isAuthenticated){t.code(401).send({error:"Unauthorized"});return}e()},getPublicKey:()=>JSON.stringify(u),logout:(a,t)=>{let e=process.env.NODE_ENV==="production"?" Secure;":"";t.header("Set-Cookie",`${d}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${e} SameSite=Lax`);let s=a.query??{},p=x(s.redirect_uri,"/");t.redirect(p)}};o.decorate("authdog",f),i()},c=y(q,{name:"@authdog/fastify",fastify:"4.x || 5.x"}),C=c;export{c as authdogPlugin,C as default};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import fp from \"fastify-plugin\";\nimport type {\n FastifyPluginCallback,\n FastifyReply,\n FastifyRequest,\n preHandlerHookHandler,\n} from \"fastify\";\nimport {\n fetchUserData,\n isAuthenticatedUserInfo,\n parseCookies,\n sanitizeRedirectPath,\n validateAndParsePublicKey,\n} from \"@authdog/node-commons\";\nimport type {\n AuthdogInstanceApi,\n AuthdogPluginOptions,\n AuthdogRequestContext,\n} from \"./types\";\n\n/** Name of the cookie carrying the session token (shared across SDKs). */\nconst SESSION_COOKIE_NAME = \"authdog-session\";\n\n/**\n * Extracts the session token, preferring the `authdog-session` cookie and\n * falling back to a `Bearer` authorization header.\n *\n * The cookie header is parsed with the shared {@link parseCookies} (splits on\n * the first `=`, URL-decodes) so JWT values containing `=` are not truncated.\n * We deliberately do NOT depend on `@fastify/cookie` being registered.\n */\nconst extractToken = (request: FastifyRequest): string | null => {\n const fromCookie = parseCookies(request.headers.cookie ?? null).find(\n (c) => c.name === SESSION_COOKIE_NAME,\n )?.value;\n if (fromCookie) {\n return fromCookie;\n }\n\n const authz = request.headers.authorization;\n if (authz && authz.startsWith(\"Bearer \")) {\n const token = authz.slice(\"Bearer \".length).trim();\n return token.length > 0 ? token : null;\n }\n\n return null;\n};\n\nconst authdogPluginCallback: FastifyPluginCallback<AuthdogPluginOptions> = (\n fastify,\n options,\n done,\n) => {\n const { publicKey, fetchUserInfo } = options;\n\n // Validate and parse the public key once at registration. This enforces the\n // trusted identity-host allowlist (SSRF / token-exfiltration protection) and\n // fails fast if the key is malformed, rather than per-request.\n const payload = validateAndParsePublicKey(publicKey);\n const { identityHost, environmentId } = payload;\n\n // Stable per-request default. Reassigned to a fresh object in the hook below,\n // so the (deprecated in v5) shared-reference pitfall does not apply.\n fastify.decorateRequest(\"authdog\", undefined);\n\n fastify.addHook(\"onRequest\", async (request) => {\n const token = extractToken(request);\n\n const context: AuthdogRequestContext = {\n token,\n user: null,\n isAuthenticated: false,\n };\n\n if (token && fetchUserInfo !== false) {\n try {\n const data = await fetchUserData(identityHost, environmentId, token);\n // Only trust a genuine success envelope (`meta.code === 200` + a user);\n // a 200 HTTP status alone is not sufficient.\n if (isAuthenticatedUserInfo(data)) {\n context.user = data.user ?? null;\n context.isAuthenticated = true;\n }\n } catch {\n // Never throw from the hook: an invalid/expired token simply yields an\n // unauthenticated context. Enforcement happens in `requireAuth`.\n }\n }\n\n request.authdog = context;\n });\n\n const requireAuth: preHandlerHookHandler = (request, reply, next) => {\n if (!request.authdog?.isAuthenticated) {\n reply.code(401).send({ error: \"Unauthorized\" });\n return;\n }\n next();\n };\n\n const logout = (request: FastifyRequest, reply: FastifyReply): void => {\n // Expire the session cookie. `Secure` is gated on production so local HTTP\n // development still clears the cookie; HttpOnly + SameSite=Lax retained.\n const secure = process.env.NODE_ENV === \"production\" ? \" Secure;\" : \"\";\n reply.header(\n \"Set-Cookie\",\n `${SESSION_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${secure} SameSite=Lax`,\n );\n\n // Sanitize the redirect target to prevent an open redirect via an\n // attacker-controlled `redirect_uri` query parameter.\n const query = (request.query ?? {}) as Record<string, unknown>;\n const target = sanitizeRedirectPath(query.redirect_uri, \"/\");\n reply.redirect(target);\n };\n\n const api: AuthdogInstanceApi = {\n requireAuth,\n getPublicKey: () => JSON.stringify(payload),\n logout,\n };\n\n fastify.decorate(\"authdog\", api);\n\n done();\n};\n\n/**\n * Authdog Fastify plugin. Register it once on your instance:\n *\n * ```ts\n * await app.register(authdogPlugin, { publicKey: process.env.AUTHDOG_PK! });\n * app.get(\"/me\", { preHandler: app.authdog.requireAuth }, async (req) => req.authdog!.user);\n * ```\n *\n * Wrapped with `fastify-plugin` so the request/instance decorations are not\n * encapsulated and are visible to sibling plugins and routes.\n */\nexport const authdogPlugin = fp(authdogPluginCallback, {\n name: \"@authdog/fastify\",\n fastify: \"4.x || 5.x\",\n});\n\nexport default authdogPlugin;\n"],"mappings":"AAAA,OAAOA,MAAQ,iBAOf,OACE,iBAAAC,EACA,2BAAAC,EACA,gBAAAC,EACA,wBAAAC,EACA,6BAAAC,MACK,wBAQP,IAAMC,EAAsB,kBAUtBC,EAAgBC,GAA2C,CAC/D,IAAMC,EAAaN,EAAaK,EAAQ,QAAQ,QAAU,IAAI,EAAE,KAC7DE,GAAMA,EAAE,OAASJ,CACpB,GAAG,MACH,GAAIG,EACF,OAAOA,EAGT,IAAME,EAAQH,EAAQ,QAAQ,cAC9B,GAAIG,GAASA,EAAM,WAAW,SAAS,EAAG,CACxC,IAAMC,EAAQD,EAAM,MAAM,CAAgB,EAAE,KAAK,EACjD,OAAOC,EAAM,OAAS,EAAIA,EAAQ,IACpC,CAEA,OAAO,IACT,EAEMC,EAAqE,CACzEC,EACAC,EACAC,IACG,CACH,GAAM,CAAE,UAAAC,EAAW,cAAAC,CAAc,EAAIH,EAK/BI,EAAUd,EAA0BY,CAAS,EAC7C,CAAE,aAAAG,EAAc,cAAAC,CAAc,EAAIF,EAIxCL,EAAQ,gBAAgB,UAAW,MAAS,EAE5CA,EAAQ,QAAQ,YAAa,MAAON,GAAY,CAC9C,IAAMI,EAAQL,EAAaC,CAAO,EAE5Bc,EAAiC,CACrC,MAAAV,EACA,KAAM,KACN,gBAAiB,EACnB,EAEA,GAAIA,GAASM,IAAkB,GAC7B,GAAI,CACF,IAAMK,EAAO,MAAMtB,EAAcmB,EAAcC,EAAeT,CAAK,EAG/DV,EAAwBqB,CAAI,IAC9BD,EAAQ,KAAOC,EAAK,MAAQ,KAC5BD,EAAQ,gBAAkB,GAE9B,MAAQ,CAGR,CAGFd,EAAQ,QAAUc,CACpB,CAAC,EA0BD,IAAME,EAA0B,CAC9B,YAzByC,CAAChB,EAASiB,EAAOC,IAAS,CACnE,GAAI,CAAClB,EAAQ,SAAS,gBAAiB,CACrCiB,EAAM,KAAK,GAAG,EAAE,KAAK,CAAE,MAAO,cAAe,CAAC,EAC9C,MACF,CACAC,EAAK,CACP,EAoBE,aAAc,IAAM,KAAK,UAAUP,CAAO,EAC1C,OAnBa,CAACX,EAAyBiB,IAA8B,CAGrE,IAAME,EAAS,QAAQ,IAAI,WAAa,aAAe,WAAa,GACpEF,EAAM,OACJ,aACA,GAAGnB,CAAmB,8DAA8DqB,CAAM,eAC5F,EAIA,IAAMC,EAASpB,EAAQ,OAAS,CAAC,EAC3BqB,EAASzB,EAAqBwB,EAAM,aAAc,GAAG,EAC3DH,EAAM,SAASI,CAAM,CACvB,CAMA,EAEAf,EAAQ,SAAS,UAAWU,CAAG,EAE/BR,EAAK,CACP,EAaac,EAAgB9B,EAAGa,EAAuB,CACrD,KAAM,mBACN,QAAS,YACX,CAAC,EAEMkB,EAAQD","names":["fp","fetchUserData","isAuthenticatedUserInfo","parseCookies","sanitizeRedirectPath","validateAndParsePublicKey","SESSION_COOKIE_NAME","extractToken","request","fromCookie","c","authz","token","authdogPluginCallback","fastify","options","done","publicKey","fetchUserInfo","payload","identityHost","environmentId","context","data","api","reply","next","secure","query","target","authdogPlugin","plugin_default"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@authdog/fastify",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Authdog Fastify SDK",
|
|
5
|
+
"source": "src/index.ts",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/authdog-labs/web-sdk.git",
|
|
23
|
+
"directory": "packages/fastify"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/authdog-labs/web-sdk/tree/main/packages/fastify#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/authdog-labs/web-sdk/issues"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"format": "prettier --config .prettierrc.json --write \"**/*.{ts,md}\"",
|
|
31
|
+
"type-check": "tsc",
|
|
32
|
+
"clean": "rm -rf dist",
|
|
33
|
+
"build": "bun run clean && tsup",
|
|
34
|
+
"ship": "bun run build && bun publish --access public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@authdog/node-commons": "workspace:*",
|
|
38
|
+
"fastify-plugin": "^5.0.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"fastify": "^4.0.0 || ^5.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.10.5",
|
|
45
|
+
"dotenv": "^16.4.7",
|
|
46
|
+
"fastify": "^5.0.0",
|
|
47
|
+
"prettier": "^3.4.2",
|
|
48
|
+
"tsup": "^8.3.5",
|
|
49
|
+
"typescript": "^5.7.2",
|
|
50
|
+
"vitest": "^2.1.8"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"registry": "https://registry.npmjs.org/",
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|