@authdog/express 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 +107 -0
- package/dist/index.d.mts +158 -0
- package/dist/index.d.ts +158 -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 +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @authdog/express
|
|
2
|
+
|
|
3
|
+
Authdog SDK for [Express](https://expressjs.com) — session middleware, an
|
|
4
|
+
authentication gate, and a logout handler for Node.js backends. Built on
|
|
5
|
+
[`@authdog/node-commons`](../node-commons), so public-key parsing, cookie
|
|
6
|
+
handling, and the trusted identity-host allowlist are shared with the rest of
|
|
7
|
+
the Authdog Web SDK.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @authdog/express express
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`express` is a peer dependency (Express 4 or 5).
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import express from "express";
|
|
21
|
+
import { createAuthdog } from "@authdog/express";
|
|
22
|
+
|
|
23
|
+
const app = express();
|
|
24
|
+
|
|
25
|
+
const authdog = createAuthdog({
|
|
26
|
+
publicKey: process.env.PK_AUTHDOG!, // pk_… (safe to expose to the browser)
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Resolve the session for every request and attach `req.authdog`.
|
|
30
|
+
app.use(authdog.attachSession());
|
|
31
|
+
|
|
32
|
+
// Public route — `req.authdog` is always present after `attachSession`.
|
|
33
|
+
app.get("/", (req, res) => {
|
|
34
|
+
res.json({ authenticated: req.authdog?.isAuthenticated ?? false });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Protected route — `requireAuth` is the real server-side enforcement point.
|
|
38
|
+
app.get("/me", authdog.requireAuth, (req, res) => {
|
|
39
|
+
res.json(req.authdog!.user);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Clears the session cookie and redirects to a sanitized `redirect_uri`.
|
|
43
|
+
app.get("/logout", authdog.logout);
|
|
44
|
+
|
|
45
|
+
app.listen(3000);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
- **`attachSession(options?)`** — reads the session token from the
|
|
51
|
+
`authdog-session` cookie or an `Authorization: Bearer <token>` header. When a
|
|
52
|
+
token is present it calls the identity provider's `userinfo` endpoint and
|
|
53
|
+
attaches the result to `req.authdog`:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
interface AuthdogRequestContext {
|
|
57
|
+
token: string | null;
|
|
58
|
+
user: unknown | null;
|
|
59
|
+
isAuthenticated: boolean;
|
|
60
|
+
userInfo?: UserInfoResponse | null;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
It **never throws and never blocks** the request — a missing, invalid, or
|
|
65
|
+
unverifiable token simply yields `isAuthenticated: false`. Mount it once,
|
|
66
|
+
early, so every downstream handler can read `req.authdog`.
|
|
67
|
+
|
|
68
|
+
- **`requireAuth`** — responds `401 { "error": "Unauthorized" }` when
|
|
69
|
+
`req.authdog?.isAuthenticated` is falsy, otherwise calls `next()`. **This is
|
|
70
|
+
the security boundary.** Client-side checks are presentational only; every
|
|
71
|
+
protected route must sit behind `requireAuth`.
|
|
72
|
+
|
|
73
|
+
- **`logout`** — expires the `authdog-session` cookie (`HttpOnly`,
|
|
74
|
+
`SameSite=Lax`, `Secure` in production) and redirects to the `redirect_uri`
|
|
75
|
+
query parameter after running it through `sanitizeRedirectPath` to prevent
|
|
76
|
+
open redirects.
|
|
77
|
+
|
|
78
|
+
## Options
|
|
79
|
+
|
|
80
|
+
### `attachSession({ fetchUser })`
|
|
81
|
+
|
|
82
|
+
By default `attachSession` performs **one outbound HTTPS request per incoming
|
|
83
|
+
request that carries a token** to resolve the full user object. For
|
|
84
|
+
high-throughput services that only need to know whether a token is present (and
|
|
85
|
+
validate it elsewhere), opt out:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
app.use(authdog.attachSession({ fetchUser: false }));
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
With `fetchUser: false`, `req.authdog.token` is populated but
|
|
92
|
+
`isAuthenticated` stays `false` and `user` stays `null` — you are responsible
|
|
93
|
+
for validating the token where it matters.
|
|
94
|
+
|
|
95
|
+
## Security
|
|
96
|
+
|
|
97
|
+
- The public key is validated and parsed **once at startup**; a malformed or
|
|
98
|
+
untrusted key (one whose identity host is not on the allowlist) throws
|
|
99
|
+
immediately rather than per-request.
|
|
100
|
+
- The bearer token is only ever sent to a trusted, `https:` identity host —
|
|
101
|
+
enforced by `@authdog/node-commons`.
|
|
102
|
+
- A request is treated as authenticated **only** when the `userinfo` envelope
|
|
103
|
+
reports success (`meta.code === 200` with a `user`).
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { UserInfoResponse, PublicKeyPayload } from '@authdog/node-commons';
|
|
2
|
+
export { PublicKeyPayload } from '@authdog/node-commons';
|
|
3
|
+
import { RequestHandler, Request, Response } from 'express';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-request authentication context attached to `req.authdog` by the
|
|
7
|
+
* `attachSession` middleware. `isAuthenticated` is the single source of truth
|
|
8
|
+
* the rest of the app (and `requireAuth`) should branch on.
|
|
9
|
+
*/
|
|
10
|
+
interface AuthdogRequestContext {
|
|
11
|
+
/** The raw session token (JWT) extracted from the cookie or bearer header. */
|
|
12
|
+
token: string | null;
|
|
13
|
+
/** The userinfo `user` object, present only when `isAuthenticated` is true. */
|
|
14
|
+
user: unknown | null;
|
|
15
|
+
/** Whether the request carries a valid, authenticated Authdog session. */
|
|
16
|
+
isAuthenticated: boolean;
|
|
17
|
+
/** The full userinfo envelope, when a userinfo fetch was performed. */
|
|
18
|
+
userInfo?: UserInfoResponse | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for {@link createAuthdog}. The `publicKey` is validated and
|
|
22
|
+
* parsed once at construction time (enforcing the trusted identity-host
|
|
23
|
+
* allowlist), so an invalid or untrusted key fails fast rather than per-request.
|
|
24
|
+
*/
|
|
25
|
+
interface AuthdogConfig {
|
|
26
|
+
/** The Authdog public key (`pk_…`). Safe to expose to the browser. */
|
|
27
|
+
publicKey: string;
|
|
28
|
+
/**
|
|
29
|
+
* The Authdog secret key. Reserved for future server-side session
|
|
30
|
+
* revocation; currently optional and unused by the request lifecycle.
|
|
31
|
+
*/
|
|
32
|
+
secretKey?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Options for the {@link AuthdogServer.attachSession} middleware factory. */
|
|
35
|
+
interface AttachSessionOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Whether to call the identity provider's `userinfo` endpoint to resolve the
|
|
38
|
+
* full user object for every authenticated request. Defaults to `true`.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ Network cost: when enabled this performs one outbound HTTPS request per
|
|
41
|
+
* incoming request that carries a token. For high-throughput services that
|
|
42
|
+
* only need to know *whether* a token is present (and validate it elsewhere),
|
|
43
|
+
* set this to `false` and perform userinfo resolution lazily where needed.
|
|
44
|
+
*/
|
|
45
|
+
fetchUser?: boolean;
|
|
46
|
+
}
|
|
47
|
+
declare global {
|
|
48
|
+
namespace Express {
|
|
49
|
+
interface Request {
|
|
50
|
+
/**
|
|
51
|
+
* Authentication context populated by Authdog's `attachSession`
|
|
52
|
+
* middleware. Always present once the middleware has run; check
|
|
53
|
+
* `req.authdog?.isAuthenticated` to gate behaviour.
|
|
54
|
+
*/
|
|
55
|
+
authdog?: AuthdogRequestContext;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The instance returned by {@link createAuthdog}. */
|
|
61
|
+
interface AuthdogServer {
|
|
62
|
+
/**
|
|
63
|
+
* Middleware that resolves the session and attaches `req.authdog`. Mount it
|
|
64
|
+
* early (typically app-wide) so downstream handlers and `requireAuth` can
|
|
65
|
+
* read the authentication context. Never throws or blocks the request.
|
|
66
|
+
*/
|
|
67
|
+
attachSession: (options?: AttachSessionOptions) => RequestHandler;
|
|
68
|
+
/**
|
|
69
|
+
* Gate middleware that returns `401` for unauthenticated requests. This is
|
|
70
|
+
* the real server-side enforcement point — place it before any protected
|
|
71
|
+
* route. Requires `attachSession` to have run first.
|
|
72
|
+
*/
|
|
73
|
+
requireAuth: RequestHandler;
|
|
74
|
+
/** Handler that clears the session cookie and performs a safe redirect. */
|
|
75
|
+
logout: (req: Request, res: Response) => void;
|
|
76
|
+
/** The validated, parsed public-key payload (`environmentId`, `identityHost`, …). */
|
|
77
|
+
getPublicKeyPayload: () => PublicKeyPayload;
|
|
78
|
+
/** The validated public-key payload as a JSON string, e.g. to inline into HTML. */
|
|
79
|
+
getPublicKey: () => string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Creates an Authdog server instance for Express.
|
|
83
|
+
*
|
|
84
|
+
* The public key is validated and parsed once here — enforcing the trusted
|
|
85
|
+
* identity-host allowlist (SSRF / token-exfiltration protection) — so a
|
|
86
|
+
* malformed or untrusted key fails fast at startup rather than on the first
|
|
87
|
+
* request.
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* const authdog = createAuthdog({ publicKey: process.env.PK_AUTHDOG! });
|
|
91
|
+
*
|
|
92
|
+
* app.use(authdog.attachSession());
|
|
93
|
+
* app.get("/me", authdog.requireAuth, (req, res) => res.json(req.authdog!.user));
|
|
94
|
+
* app.get("/logout", authdog.logout);
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
declare const createAuthdog: (config: AuthdogConfig) => AuthdogServer;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Builds the `attachSession` middleware. It never throws and never short-
|
|
101
|
+
* circuits the request: it resolves the session (if any) and attaches the
|
|
102
|
+
* result to `req.authdog`, leaving the decision of what to do with an
|
|
103
|
+
* unauthenticated request to `requireAuth` or your route handlers.
|
|
104
|
+
*
|
|
105
|
+
* When `fetchUser` is enabled (the default) it calls the identity provider's
|
|
106
|
+
* `userinfo` endpoint and only marks the request authenticated when the
|
|
107
|
+
* envelope reports success (`isAuthenticatedUserInfo`). Any network or parse
|
|
108
|
+
* failure degrades to an unauthenticated context rather than a 500.
|
|
109
|
+
*/
|
|
110
|
+
declare const createAttachSession: (payload: PublicKeyPayload, options?: AttachSessionOptions) => RequestHandler;
|
|
111
|
+
/**
|
|
112
|
+
* Gate middleware that enforces authentication. Responds with `401` JSON when
|
|
113
|
+
* the request has no valid Authdog session and calls `next()` otherwise.
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ This is the real server-side enforcement point. Client-side checks are
|
|
116
|
+
* presentational only; every protected route MUST sit behind `requireAuth`
|
|
117
|
+
* (after `attachSession` has run) for the protection to be real.
|
|
118
|
+
*/
|
|
119
|
+
declare const requireAuth: RequestHandler;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Express handler that clears the Authdog session cookie and redirects to a
|
|
123
|
+
* safe, same-origin location.
|
|
124
|
+
*
|
|
125
|
+
* The cookie is expired in place with the same security attributes it was set
|
|
126
|
+
* with (`HttpOnly`, `SameSite=Lax`, and `Secure` in production) so browsers
|
|
127
|
+
* actually drop it. `Secure` is gated on `NODE_ENV === "production"` so local
|
|
128
|
+
* HTTP development still clears the cookie.
|
|
129
|
+
*
|
|
130
|
+
* The redirect target is taken from the `redirect_uri` query parameter and run
|
|
131
|
+
* through `sanitizeRedirectPath` to prevent an open redirect via an
|
|
132
|
+
* attacker-controlled value (falls back to `/`).
|
|
133
|
+
*/
|
|
134
|
+
declare const logoutHandler: (req: Request, res: Response) => void;
|
|
135
|
+
|
|
136
|
+
/** Name of the cookie that carries the Authdog session token. */
|
|
137
|
+
declare const SESSION_COOKIE_NAME = "authdog-session";
|
|
138
|
+
/**
|
|
139
|
+
* Extracts the session token from an incoming request. The token may arrive
|
|
140
|
+
* either as the `authdog-session` cookie (set server-side, HttpOnly) or as an
|
|
141
|
+
* `Authorization: Bearer <token>` header — the latter covers API clients and
|
|
142
|
+
* mobile callers that do not use cookies.
|
|
143
|
+
*
|
|
144
|
+
* Cookie parsing goes through `parseCookies`, which splits on the first `=` and
|
|
145
|
+
* URL-decodes values, correctly handling tokens that themselves contain `=`
|
|
146
|
+
* (e.g. base64 / JWT padding).
|
|
147
|
+
*/
|
|
148
|
+
declare const getSessionToken: (req: Request) => string | null;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Decodes and validates an Authdog public key. Delegates to the hardened
|
|
152
|
+
* shared parser in @authdog/node-commons, which validates the payload and
|
|
153
|
+
* enforces a trusted identity-host allowlist (SSRF / token-exfiltration
|
|
154
|
+
* protection) rather than blindly decoding base64/JSON.
|
|
155
|
+
*/
|
|
156
|
+
declare const getPublicKeyPayload: (publicKey: string) => PublicKeyPayload;
|
|
157
|
+
|
|
158
|
+
export { type AttachSessionOptions, type AuthdogConfig, type AuthdogRequestContext, type AuthdogServer, SESSION_COOKIE_NAME, createAttachSession, createAuthdog, getPublicKeyPayload, getSessionToken, logoutHandler, requireAuth };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { UserInfoResponse, PublicKeyPayload } from '@authdog/node-commons';
|
|
2
|
+
export { PublicKeyPayload } from '@authdog/node-commons';
|
|
3
|
+
import { RequestHandler, Request, Response } from 'express';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-request authentication context attached to `req.authdog` by the
|
|
7
|
+
* `attachSession` middleware. `isAuthenticated` is the single source of truth
|
|
8
|
+
* the rest of the app (and `requireAuth`) should branch on.
|
|
9
|
+
*/
|
|
10
|
+
interface AuthdogRequestContext {
|
|
11
|
+
/** The raw session token (JWT) extracted from the cookie or bearer header. */
|
|
12
|
+
token: string | null;
|
|
13
|
+
/** The userinfo `user` object, present only when `isAuthenticated` is true. */
|
|
14
|
+
user: unknown | null;
|
|
15
|
+
/** Whether the request carries a valid, authenticated Authdog session. */
|
|
16
|
+
isAuthenticated: boolean;
|
|
17
|
+
/** The full userinfo envelope, when a userinfo fetch was performed. */
|
|
18
|
+
userInfo?: UserInfoResponse | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for {@link createAuthdog}. The `publicKey` is validated and
|
|
22
|
+
* parsed once at construction time (enforcing the trusted identity-host
|
|
23
|
+
* allowlist), so an invalid or untrusted key fails fast rather than per-request.
|
|
24
|
+
*/
|
|
25
|
+
interface AuthdogConfig {
|
|
26
|
+
/** The Authdog public key (`pk_…`). Safe to expose to the browser. */
|
|
27
|
+
publicKey: string;
|
|
28
|
+
/**
|
|
29
|
+
* The Authdog secret key. Reserved for future server-side session
|
|
30
|
+
* revocation; currently optional and unused by the request lifecycle.
|
|
31
|
+
*/
|
|
32
|
+
secretKey?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Options for the {@link AuthdogServer.attachSession} middleware factory. */
|
|
35
|
+
interface AttachSessionOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Whether to call the identity provider's `userinfo` endpoint to resolve the
|
|
38
|
+
* full user object for every authenticated request. Defaults to `true`.
|
|
39
|
+
*
|
|
40
|
+
* ⚠️ Network cost: when enabled this performs one outbound HTTPS request per
|
|
41
|
+
* incoming request that carries a token. For high-throughput services that
|
|
42
|
+
* only need to know *whether* a token is present (and validate it elsewhere),
|
|
43
|
+
* set this to `false` and perform userinfo resolution lazily where needed.
|
|
44
|
+
*/
|
|
45
|
+
fetchUser?: boolean;
|
|
46
|
+
}
|
|
47
|
+
declare global {
|
|
48
|
+
namespace Express {
|
|
49
|
+
interface Request {
|
|
50
|
+
/**
|
|
51
|
+
* Authentication context populated by Authdog's `attachSession`
|
|
52
|
+
* middleware. Always present once the middleware has run; check
|
|
53
|
+
* `req.authdog?.isAuthenticated` to gate behaviour.
|
|
54
|
+
*/
|
|
55
|
+
authdog?: AuthdogRequestContext;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The instance returned by {@link createAuthdog}. */
|
|
61
|
+
interface AuthdogServer {
|
|
62
|
+
/**
|
|
63
|
+
* Middleware that resolves the session and attaches `req.authdog`. Mount it
|
|
64
|
+
* early (typically app-wide) so downstream handlers and `requireAuth` can
|
|
65
|
+
* read the authentication context. Never throws or blocks the request.
|
|
66
|
+
*/
|
|
67
|
+
attachSession: (options?: AttachSessionOptions) => RequestHandler;
|
|
68
|
+
/**
|
|
69
|
+
* Gate middleware that returns `401` for unauthenticated requests. This is
|
|
70
|
+
* the real server-side enforcement point — place it before any protected
|
|
71
|
+
* route. Requires `attachSession` to have run first.
|
|
72
|
+
*/
|
|
73
|
+
requireAuth: RequestHandler;
|
|
74
|
+
/** Handler that clears the session cookie and performs a safe redirect. */
|
|
75
|
+
logout: (req: Request, res: Response) => void;
|
|
76
|
+
/** The validated, parsed public-key payload (`environmentId`, `identityHost`, …). */
|
|
77
|
+
getPublicKeyPayload: () => PublicKeyPayload;
|
|
78
|
+
/** The validated public-key payload as a JSON string, e.g. to inline into HTML. */
|
|
79
|
+
getPublicKey: () => string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Creates an Authdog server instance for Express.
|
|
83
|
+
*
|
|
84
|
+
* The public key is validated and parsed once here — enforcing the trusted
|
|
85
|
+
* identity-host allowlist (SSRF / token-exfiltration protection) — so a
|
|
86
|
+
* malformed or untrusted key fails fast at startup rather than on the first
|
|
87
|
+
* request.
|
|
88
|
+
*
|
|
89
|
+
* ```ts
|
|
90
|
+
* const authdog = createAuthdog({ publicKey: process.env.PK_AUTHDOG! });
|
|
91
|
+
*
|
|
92
|
+
* app.use(authdog.attachSession());
|
|
93
|
+
* app.get("/me", authdog.requireAuth, (req, res) => res.json(req.authdog!.user));
|
|
94
|
+
* app.get("/logout", authdog.logout);
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
declare const createAuthdog: (config: AuthdogConfig) => AuthdogServer;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Builds the `attachSession` middleware. It never throws and never short-
|
|
101
|
+
* circuits the request: it resolves the session (if any) and attaches the
|
|
102
|
+
* result to `req.authdog`, leaving the decision of what to do with an
|
|
103
|
+
* unauthenticated request to `requireAuth` or your route handlers.
|
|
104
|
+
*
|
|
105
|
+
* When `fetchUser` is enabled (the default) it calls the identity provider's
|
|
106
|
+
* `userinfo` endpoint and only marks the request authenticated when the
|
|
107
|
+
* envelope reports success (`isAuthenticatedUserInfo`). Any network or parse
|
|
108
|
+
* failure degrades to an unauthenticated context rather than a 500.
|
|
109
|
+
*/
|
|
110
|
+
declare const createAttachSession: (payload: PublicKeyPayload, options?: AttachSessionOptions) => RequestHandler;
|
|
111
|
+
/**
|
|
112
|
+
* Gate middleware that enforces authentication. Responds with `401` JSON when
|
|
113
|
+
* the request has no valid Authdog session and calls `next()` otherwise.
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ This is the real server-side enforcement point. Client-side checks are
|
|
116
|
+
* presentational only; every protected route MUST sit behind `requireAuth`
|
|
117
|
+
* (after `attachSession` has run) for the protection to be real.
|
|
118
|
+
*/
|
|
119
|
+
declare const requireAuth: RequestHandler;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Express handler that clears the Authdog session cookie and redirects to a
|
|
123
|
+
* safe, same-origin location.
|
|
124
|
+
*
|
|
125
|
+
* The cookie is expired in place with the same security attributes it was set
|
|
126
|
+
* with (`HttpOnly`, `SameSite=Lax`, and `Secure` in production) so browsers
|
|
127
|
+
* actually drop it. `Secure` is gated on `NODE_ENV === "production"` so local
|
|
128
|
+
* HTTP development still clears the cookie.
|
|
129
|
+
*
|
|
130
|
+
* The redirect target is taken from the `redirect_uri` query parameter and run
|
|
131
|
+
* through `sanitizeRedirectPath` to prevent an open redirect via an
|
|
132
|
+
* attacker-controlled value (falls back to `/`).
|
|
133
|
+
*/
|
|
134
|
+
declare const logoutHandler: (req: Request, res: Response) => void;
|
|
135
|
+
|
|
136
|
+
/** Name of the cookie that carries the Authdog session token. */
|
|
137
|
+
declare const SESSION_COOKIE_NAME = "authdog-session";
|
|
138
|
+
/**
|
|
139
|
+
* Extracts the session token from an incoming request. The token may arrive
|
|
140
|
+
* either as the `authdog-session` cookie (set server-side, HttpOnly) or as an
|
|
141
|
+
* `Authorization: Bearer <token>` header — the latter covers API clients and
|
|
142
|
+
* mobile callers that do not use cookies.
|
|
143
|
+
*
|
|
144
|
+
* Cookie parsing goes through `parseCookies`, which splits on the first `=` and
|
|
145
|
+
* URL-decodes values, correctly handling tokens that themselves contain `=`
|
|
146
|
+
* (e.g. base64 / JWT padding).
|
|
147
|
+
*/
|
|
148
|
+
declare const getSessionToken: (req: Request) => string | null;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Decodes and validates an Authdog public key. Delegates to the hardened
|
|
152
|
+
* shared parser in @authdog/node-commons, which validates the payload and
|
|
153
|
+
* enforces a trusted identity-host allowlist (SSRF / token-exfiltration
|
|
154
|
+
* protection) rather than blindly decoding base64/JSON.
|
|
155
|
+
*/
|
|
156
|
+
declare const getPublicKeyPayload: (publicKey: string) => PublicKeyPayload;
|
|
157
|
+
|
|
158
|
+
export { type AttachSessionOptions, type AuthdogConfig, type AuthdogRequestContext, type AuthdogServer, SESSION_COOKIE_NAME, createAttachSession, createAuthdog, getPublicKeyPayload, getSessionToken, logoutHandler, requireAuth };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var f=Object.defineProperty;var x=Object.getOwnPropertyDescriptor;var R=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var O=(t,e)=>{for(var o in e)f(t,o,{get:e[o],enumerable:!0})},b=(t,e,o,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of R(e))!K.call(t,s)&&s!==o&&f(t,s,{get:()=>e[s],enumerable:!(r=x(e,s))||r.enumerable});return t};var q=t=>b(f({},"__esModule",{value:!0}),t);var N={};O(N,{SESSION_COOKIE_NAME:()=>i,createAttachSession:()=>p,createAuthdog:()=>S,getPublicKeyPayload:()=>u,getSessionToken:()=>a,logoutHandler:()=>c,requireAuth:()=>d});module.exports=q(N);var g=require("@authdog/node-commons"),u=t=>(0,g.validateAndParsePublicKey)(t);var P=require("@authdog/node-commons");var A=require("@authdog/node-commons"),i="authdog-session",a=t=>{let e=t.headers.authorization;if(e&&/^Bearer\s+/i.test(e)){let s=e.replace(/^Bearer\s+/i,"").trim();if(s)return s}let o=t.headers.cookie??null;return o?(0,A.parseCookies)(o).find(s=>s.name===i)?.value??null:null};var c=(t,e)=>{let o=process.env.NODE_ENV==="production"?" Secure;":"";e.setHeader("Set-Cookie",`${i}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${o} SameSite=Lax`);let r=(0,P.sanitizeRedirectPath)(t.query.redirect_uri,"/");e.redirect(302,r)};var l=require("@authdog/node-commons");var H={token:null,user:null,isAuthenticated:!1,userInfo:null},p=(t,e={})=>{let o=e.fetchUser??!0;return async(r,s,h)=>{let n=a(r);if(!n)return r.authdog={...H},h();if(!o)return r.authdog={token:n,user:null,isAuthenticated:!1,userInfo:null},h();try{let y=await(0,l.fetchUserData)(t.identityHost,t.environmentId,n),m=(0,l.isAuthenticatedUserInfo)(y);r.authdog={token:n,user:m?y.user??null:null,isAuthenticated:m,userInfo:y}}catch{r.authdog={token:n,user:null,isAuthenticated:!1,userInfo:null}}return h()}},d=(t,e,o)=>{if(!t.authdog?.isAuthenticated){e.status(401).json({error:"Unauthorized"});return}o()};var S=t=>{if(!t.publicKey)throw new Error("Public key is not defined");let e=u(t.publicKey);return{attachSession:o=>p(e,o),requireAuth:d,logout:c,getPublicKeyPayload:()=>e,getPublicKey:()=>JSON.stringify(e)}};0&&(module.exports={SESSION_COOKIE_NAME,createAttachSession,createAuthdog,getPublicKeyPayload,getSessionToken,logoutHandler,requireAuth});
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/commons.ts","../src/logout.ts","../src/cookies.ts","../src/middleware.ts","../src/authdog.ts"],"sourcesContent":["export { createAuthdog } from \"./authdog\";\nexport type { AuthdogServer } from \"./authdog\";\nexport { createAttachSession, requireAuth } from \"./middleware\";\nexport { logoutHandler } from \"./logout\";\nexport { getSessionToken, SESSION_COOKIE_NAME } from \"./cookies\";\nexport { getPublicKeyPayload } from \"./commons\";\nexport type { PublicKeyPayload } from \"./commons\";\nexport type {\n AuthdogConfig,\n AuthdogRequestContext,\n AttachSessionOptions,\n} from \"./types\";\n","import {\n validateAndParsePublicKey,\n type PublicKeyPayload,\n} from \"@authdog/node-commons\";\n\nexport type { PublicKeyPayload };\n\n/**\n * Decodes and validates an Authdog public key. Delegates to the hardened\n * shared parser in @authdog/node-commons, which validates the payload and\n * enforces a trusted identity-host allowlist (SSRF / token-exfiltration\n * protection) rather than blindly decoding base64/JSON.\n */\nexport const getPublicKeyPayload = (publicKey: string): PublicKeyPayload => {\n return validateAndParsePublicKey(publicKey);\n};\n","import { sanitizeRedirectPath } from \"@authdog/node-commons\";\nimport type { Request, Response } from \"express\";\nimport { SESSION_COOKIE_NAME } from \"./cookies\";\n\n/**\n * Express handler that clears the Authdog session cookie and redirects to a\n * safe, same-origin location.\n *\n * The cookie is expired in place with the same security attributes it was set\n * with (`HttpOnly`, `SameSite=Lax`, and `Secure` in production) so browsers\n * actually drop it. `Secure` is gated on `NODE_ENV === \"production\"` so local\n * HTTP development still clears the cookie.\n *\n * The redirect target is taken from the `redirect_uri` query parameter and run\n * through `sanitizeRedirectPath` to prevent an open redirect via an\n * attacker-controlled value (falls back to `/`).\n */\nexport const logoutHandler = (req: Request, res: Response): void => {\n const secure = process.env.NODE_ENV === \"production\" ? \" Secure;\" : \"\";\n\n res.setHeader(\n \"Set-Cookie\",\n `${SESSION_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${secure} SameSite=Lax`,\n );\n\n const redirectUrl = sanitizeRedirectPath(req.query.redirect_uri, \"/\");\n\n res.redirect(302, redirectUrl);\n};\n","import { parseCookies } from \"@authdog/node-commons\";\nimport type { Request } from \"express\";\n\n/** Name of the cookie that carries the Authdog session token. */\nexport const SESSION_COOKIE_NAME = \"authdog-session\";\n\n/**\n * Extracts the session token from an incoming request. The token may arrive\n * either as the `authdog-session` cookie (set server-side, HttpOnly) or as an\n * `Authorization: Bearer <token>` header — the latter covers API clients and\n * mobile callers that do not use cookies.\n *\n * Cookie parsing goes through `parseCookies`, which splits on the first `=` and\n * URL-decodes values, correctly handling tokens that themselves contain `=`\n * (e.g. base64 / JWT padding).\n */\nexport const getSessionToken = (req: Request): string | null => {\n // Prefer an explicit bearer token when present.\n const authHeader = req.headers.authorization;\n if (authHeader && /^Bearer\\s+/i.test(authHeader)) {\n const token = authHeader.replace(/^Bearer\\s+/i, \"\").trim();\n if (token) {\n return token;\n }\n }\n\n const cookieHeader = req.headers.cookie ?? null;\n if (!cookieHeader) {\n return null;\n }\n\n const cookies = parseCookies(cookieHeader);\n return cookies.find((c) => c.name === SESSION_COOKIE_NAME)?.value ?? null;\n};\n","import {\n fetchUserData,\n isAuthenticatedUserInfo,\n type PublicKeyPayload,\n} from \"@authdog/node-commons\";\nimport type { NextFunction, Request, RequestHandler, Response } from \"express\";\nimport { getSessionToken } from \"./cookies\";\nimport type { AttachSessionOptions, AuthdogRequestContext } from \"./types\";\n\n/** The context attached when no usable token is present on the request. */\nconst ANONYMOUS: AuthdogRequestContext = {\n token: null,\n user: null,\n isAuthenticated: false,\n userInfo: null,\n};\n\n/**\n * Builds the `attachSession` middleware. It never throws and never short-\n * circuits the request: it resolves the session (if any) and attaches the\n * result to `req.authdog`, leaving the decision of what to do with an\n * unauthenticated request to `requireAuth` or your route handlers.\n *\n * When `fetchUser` is enabled (the default) it calls the identity provider's\n * `userinfo` endpoint and only marks the request authenticated when the\n * envelope reports success (`isAuthenticatedUserInfo`). Any network or parse\n * failure degrades to an unauthenticated context rather than a 500.\n */\nexport const createAttachSession = (\n payload: PublicKeyPayload,\n options: AttachSessionOptions = {},\n): RequestHandler => {\n const fetchUser = options.fetchUser ?? true;\n\n return async (req: Request, _res: Response, next: NextFunction) => {\n const token = getSessionToken(req);\n\n if (!token) {\n req.authdog = { ...ANONYMOUS };\n return next();\n }\n\n // Without a userinfo round-trip we cannot vouch for the token's validity,\n // so we surface the token but leave `isAuthenticated` false. Callers that\n // opt out of fetching are expected to validate the token themselves.\n if (!fetchUser) {\n req.authdog = {\n token,\n user: null,\n isAuthenticated: false,\n userInfo: null,\n };\n return next();\n }\n\n try {\n const userInfo = await fetchUserData(\n payload.identityHost,\n payload.environmentId,\n token,\n );\n\n const authenticated = isAuthenticatedUserInfo(userInfo);\n\n req.authdog = {\n token,\n user: authenticated ? (userInfo.user ?? null) : null,\n isAuthenticated: authenticated,\n userInfo,\n };\n } catch {\n // A failed or untrusted userinfo lookup is treated as \"not authenticated\"\n // — never as a server error and never as an authenticated session.\n req.authdog = { token, user: null, isAuthenticated: false, userInfo: null };\n }\n\n return next();\n };\n};\n\n/**\n * Gate middleware that enforces authentication. Responds with `401` JSON when\n * the request has no valid Authdog session and calls `next()` otherwise.\n *\n * ⚠️ This is the real server-side enforcement point. Client-side checks are\n * presentational only; every protected route MUST sit behind `requireAuth`\n * (after `attachSession` has run) for the protection to be real.\n */\nexport const requireAuth: RequestHandler = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => {\n if (!req.authdog?.isAuthenticated) {\n res.status(401).json({ error: \"Unauthorized\" });\n return;\n }\n next();\n};\n","import type { PublicKeyPayload } from \"@authdog/node-commons\";\nimport type { Request, RequestHandler, Response } from \"express\";\nimport { getPublicKeyPayload } from \"./commons\";\nimport { logoutHandler } from \"./logout\";\nimport { createAttachSession, requireAuth } from \"./middleware\";\nimport type { AttachSessionOptions, AuthdogConfig } from \"./types\";\n\n/** The instance returned by {@link createAuthdog}. */\nexport interface AuthdogServer {\n /**\n * Middleware that resolves the session and attaches `req.authdog`. Mount it\n * early (typically app-wide) so downstream handlers and `requireAuth` can\n * read the authentication context. Never throws or blocks the request.\n */\n attachSession: (options?: AttachSessionOptions) => RequestHandler;\n /**\n * Gate middleware that returns `401` for unauthenticated requests. This is\n * the real server-side enforcement point — place it before any protected\n * route. Requires `attachSession` to have run first.\n */\n requireAuth: RequestHandler;\n /** Handler that clears the session cookie and performs a safe redirect. */\n logout: (req: Request, res: Response) => void;\n /** The validated, parsed public-key payload (`environmentId`, `identityHost`, …). */\n getPublicKeyPayload: () => PublicKeyPayload;\n /** The validated public-key payload as a JSON string, e.g. to inline into HTML. */\n getPublicKey: () => string;\n}\n\n/**\n * Creates an Authdog server instance for Express.\n *\n * The public key is validated and parsed once here — enforcing the trusted\n * identity-host allowlist (SSRF / token-exfiltration protection) — so a\n * malformed or untrusted key fails fast at startup rather than on the first\n * request.\n *\n * ```ts\n * const authdog = createAuthdog({ publicKey: process.env.PK_AUTHDOG! });\n *\n * app.use(authdog.attachSession());\n * app.get(\"/me\", authdog.requireAuth, (req, res) => res.json(req.authdog!.user));\n * app.get(\"/logout\", authdog.logout);\n * ```\n */\nexport const createAuthdog = (config: AuthdogConfig): AuthdogServer => {\n if (!config.publicKey) {\n throw new Error(\"Public key is not defined\");\n }\n\n // Validate + parse eagerly so an invalid/untrusted key throws at startup.\n const payload = getPublicKeyPayload(config.publicKey);\n\n return {\n attachSession: (options?: AttachSessionOptions) =>\n createAttachSession(payload, options),\n requireAuth,\n logout: logoutHandler,\n getPublicKeyPayload: () => payload,\n getPublicKey: () => JSON.stringify(payload),\n };\n};\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,yBAAAE,EAAA,wBAAAC,EAAA,kBAAAC,EAAA,wBAAAC,EAAA,oBAAAC,EAAA,kBAAAC,EAAA,gBAAAC,IAAA,eAAAC,EAAAT,GCAA,IAAAU,EAGO,iCAUMC,EAAuBC,MAC3B,6BAA0BA,CAAS,ECd5C,IAAAC,EAAqC,iCCArC,IAAAC,EAA6B,iCAIhBC,EAAsB,kBAYtBC,EAAmBC,GAAgC,CAE9D,IAAMC,EAAaD,EAAI,QAAQ,cAC/B,GAAIC,GAAc,cAAc,KAAKA,CAAU,EAAG,CAChD,IAAMC,EAAQD,EAAW,QAAQ,cAAe,EAAE,EAAE,KAAK,EACzD,GAAIC,EACF,OAAOA,CAEX,CAEA,IAAMC,EAAeH,EAAI,QAAQ,QAAU,KAC3C,OAAKG,KAIW,gBAAaA,CAAY,EAC1B,KAAMC,GAAMA,EAAE,OAASN,CAAmB,GAAG,OAAS,KAJ5D,IAKX,EDhBO,IAAMO,EAAgB,CAACC,EAAcC,IAAwB,CAClE,IAAMC,EAAS,QAAQ,IAAI,WAAa,aAAe,WAAa,GAEpED,EAAI,UACF,aACA,GAAGE,CAAmB,8DAA8DD,CAAM,eAC5F,EAEA,IAAME,KAAc,wBAAqBJ,EAAI,MAAM,aAAc,GAAG,EAEpEC,EAAI,SAAS,IAAKG,CAAW,CAC/B,EE5BA,IAAAC,EAIO,iCAMP,IAAMC,EAAmC,CACvC,MAAO,KACP,KAAM,KACN,gBAAiB,GACjB,SAAU,IACZ,EAaaC,EAAsB,CACjCC,EACAC,EAAgC,CAAC,IACd,CACnB,IAAMC,EAAYD,EAAQ,WAAa,GAEvC,MAAO,OAAOE,EAAcC,EAAgBC,IAAuB,CACjE,IAAMC,EAAQC,EAAgBJ,CAAG,EAEjC,GAAI,CAACG,EACH,OAAAH,EAAI,QAAU,CAAE,GAAGL,CAAU,EACtBO,EAAK,EAMd,GAAI,CAACH,EACH,OAAAC,EAAI,QAAU,CACZ,MAAAG,EACA,KAAM,KACN,gBAAiB,GACjB,SAAU,IACZ,EACOD,EAAK,EAGd,GAAI,CACF,IAAMG,EAAW,QAAM,iBACrBR,EAAQ,aACRA,EAAQ,cACRM,CACF,EAEMG,KAAgB,2BAAwBD,CAAQ,EAEtDL,EAAI,QAAU,CACZ,MAAAG,EACA,KAAMG,EAAiBD,EAAS,MAAQ,KAAQ,KAChD,gBAAiBC,EACjB,SAAAD,CACF,CACF,MAAQ,CAGNL,EAAI,QAAU,CAAE,MAAAG,EAAO,KAAM,KAAM,gBAAiB,GAAO,SAAU,IAAK,CAC5E,CAEA,OAAOD,EAAK,CACd,CACF,EAUaK,EAA8B,CACzCP,EACAQ,EACAN,IACG,CACH,GAAI,CAACF,EAAI,SAAS,gBAAiB,CACjCQ,EAAI,OAAO,GAAG,EAAE,KAAK,CAAE,MAAO,cAAe,CAAC,EAC9C,MACF,CACAN,EAAK,CACP,ECrDO,IAAMO,EAAiBC,GAAyC,CACrE,GAAI,CAACA,EAAO,UACV,MAAM,IAAI,MAAM,2BAA2B,EAI7C,IAAMC,EAAUC,EAAoBF,EAAO,SAAS,EAEpD,MAAO,CACL,cAAgBG,GACdC,EAAoBH,EAASE,CAAO,EACtC,YAAAE,EACA,OAAQC,EACR,oBAAqB,IAAML,EAC3B,aAAc,IAAM,KAAK,UAAUA,CAAO,CAC5C,CACF","names":["index_exports","__export","SESSION_COOKIE_NAME","createAttachSession","createAuthdog","getPublicKeyPayload","getSessionToken","logoutHandler","requireAuth","__toCommonJS","import_node_commons","getPublicKeyPayload","publicKey","import_node_commons","import_node_commons","SESSION_COOKIE_NAME","getSessionToken","req","authHeader","token","cookieHeader","c","logoutHandler","req","res","secure","SESSION_COOKIE_NAME","redirectUrl","import_node_commons","ANONYMOUS","createAttachSession","payload","options","fetchUser","req","_res","next","token","getSessionToken","userInfo","authenticated","requireAuth","res","createAuthdog","config","payload","getPublicKeyPayload","options","createAttachSession","requireAuth","logoutHandler"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{validateAndParsePublicKey as f}from"@authdog/node-commons";var c=e=>f(e);import{sanitizeRedirectPath as g}from"@authdog/node-commons";import{parseCookies as m}from"@authdog/node-commons";var i="authdog-session",l=e=>{let t=e.headers.authorization;if(t&&/^Bearer\s+/i.test(t)){let s=t.replace(/^Bearer\s+/i,"").trim();if(s)return s}let o=e.headers.cookie??null;return o?m(o).find(s=>s.name===i)?.value??null:null};var p=(e,t)=>{let o=process.env.NODE_ENV==="production"?" Secure;":"";t.setHeader("Set-Cookie",`${i}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${o} SameSite=Lax`);let r=g(e.query.redirect_uri,"/");t.redirect(302,r)};import{fetchUserData as A,isAuthenticatedUserInfo as P}from"@authdog/node-commons";var S={token:null,user:null,isAuthenticated:!1,userInfo:null},d=(e,t={})=>{let o=t.fetchUser??!0;return async(r,s,u)=>{let n=l(r);if(!n)return r.authdog={...S},u();if(!o)return r.authdog={token:n,user:null,isAuthenticated:!1,userInfo:null},u();try{let a=await A(e.identityHost,e.environmentId,n),y=P(a);r.authdog={token:n,user:y?a.user??null:null,isAuthenticated:y,userInfo:a}}catch{r.authdog={token:n,user:null,isAuthenticated:!1,userInfo:null}}return u()}},h=(e,t,o)=>{if(!e.authdog?.isAuthenticated){t.status(401).json({error:"Unauthorized"});return}o()};var x=e=>{if(!e.publicKey)throw new Error("Public key is not defined");let t=c(e.publicKey);return{attachSession:o=>d(t,o),requireAuth:h,logout:p,getPublicKeyPayload:()=>t,getPublicKey:()=>JSON.stringify(t)}};export{i as SESSION_COOKIE_NAME,d as createAttachSession,x as createAuthdog,c as getPublicKeyPayload,l as getSessionToken,p as logoutHandler,h as requireAuth};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commons.ts","../src/logout.ts","../src/cookies.ts","../src/middleware.ts","../src/authdog.ts"],"sourcesContent":["import {\n validateAndParsePublicKey,\n type PublicKeyPayload,\n} from \"@authdog/node-commons\";\n\nexport type { PublicKeyPayload };\n\n/**\n * Decodes and validates an Authdog public key. Delegates to the hardened\n * shared parser in @authdog/node-commons, which validates the payload and\n * enforces a trusted identity-host allowlist (SSRF / token-exfiltration\n * protection) rather than blindly decoding base64/JSON.\n */\nexport const getPublicKeyPayload = (publicKey: string): PublicKeyPayload => {\n return validateAndParsePublicKey(publicKey);\n};\n","import { sanitizeRedirectPath } from \"@authdog/node-commons\";\nimport type { Request, Response } from \"express\";\nimport { SESSION_COOKIE_NAME } from \"./cookies\";\n\n/**\n * Express handler that clears the Authdog session cookie and redirects to a\n * safe, same-origin location.\n *\n * The cookie is expired in place with the same security attributes it was set\n * with (`HttpOnly`, `SameSite=Lax`, and `Secure` in production) so browsers\n * actually drop it. `Secure` is gated on `NODE_ENV === \"production\"` so local\n * HTTP development still clears the cookie.\n *\n * The redirect target is taken from the `redirect_uri` query parameter and run\n * through `sanitizeRedirectPath` to prevent an open redirect via an\n * attacker-controlled value (falls back to `/`).\n */\nexport const logoutHandler = (req: Request, res: Response): void => {\n const secure = process.env.NODE_ENV === \"production\" ? \" Secure;\" : \"\";\n\n res.setHeader(\n \"Set-Cookie\",\n `${SESSION_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly;${secure} SameSite=Lax`,\n );\n\n const redirectUrl = sanitizeRedirectPath(req.query.redirect_uri, \"/\");\n\n res.redirect(302, redirectUrl);\n};\n","import { parseCookies } from \"@authdog/node-commons\";\nimport type { Request } from \"express\";\n\n/** Name of the cookie that carries the Authdog session token. */\nexport const SESSION_COOKIE_NAME = \"authdog-session\";\n\n/**\n * Extracts the session token from an incoming request. The token may arrive\n * either as the `authdog-session` cookie (set server-side, HttpOnly) or as an\n * `Authorization: Bearer <token>` header — the latter covers API clients and\n * mobile callers that do not use cookies.\n *\n * Cookie parsing goes through `parseCookies`, which splits on the first `=` and\n * URL-decodes values, correctly handling tokens that themselves contain `=`\n * (e.g. base64 / JWT padding).\n */\nexport const getSessionToken = (req: Request): string | null => {\n // Prefer an explicit bearer token when present.\n const authHeader = req.headers.authorization;\n if (authHeader && /^Bearer\\s+/i.test(authHeader)) {\n const token = authHeader.replace(/^Bearer\\s+/i, \"\").trim();\n if (token) {\n return token;\n }\n }\n\n const cookieHeader = req.headers.cookie ?? null;\n if (!cookieHeader) {\n return null;\n }\n\n const cookies = parseCookies(cookieHeader);\n return cookies.find((c) => c.name === SESSION_COOKIE_NAME)?.value ?? null;\n};\n","import {\n fetchUserData,\n isAuthenticatedUserInfo,\n type PublicKeyPayload,\n} from \"@authdog/node-commons\";\nimport type { NextFunction, Request, RequestHandler, Response } from \"express\";\nimport { getSessionToken } from \"./cookies\";\nimport type { AttachSessionOptions, AuthdogRequestContext } from \"./types\";\n\n/** The context attached when no usable token is present on the request. */\nconst ANONYMOUS: AuthdogRequestContext = {\n token: null,\n user: null,\n isAuthenticated: false,\n userInfo: null,\n};\n\n/**\n * Builds the `attachSession` middleware. It never throws and never short-\n * circuits the request: it resolves the session (if any) and attaches the\n * result to `req.authdog`, leaving the decision of what to do with an\n * unauthenticated request to `requireAuth` or your route handlers.\n *\n * When `fetchUser` is enabled (the default) it calls the identity provider's\n * `userinfo` endpoint and only marks the request authenticated when the\n * envelope reports success (`isAuthenticatedUserInfo`). Any network or parse\n * failure degrades to an unauthenticated context rather than a 500.\n */\nexport const createAttachSession = (\n payload: PublicKeyPayload,\n options: AttachSessionOptions = {},\n): RequestHandler => {\n const fetchUser = options.fetchUser ?? true;\n\n return async (req: Request, _res: Response, next: NextFunction) => {\n const token = getSessionToken(req);\n\n if (!token) {\n req.authdog = { ...ANONYMOUS };\n return next();\n }\n\n // Without a userinfo round-trip we cannot vouch for the token's validity,\n // so we surface the token but leave `isAuthenticated` false. Callers that\n // opt out of fetching are expected to validate the token themselves.\n if (!fetchUser) {\n req.authdog = {\n token,\n user: null,\n isAuthenticated: false,\n userInfo: null,\n };\n return next();\n }\n\n try {\n const userInfo = await fetchUserData(\n payload.identityHost,\n payload.environmentId,\n token,\n );\n\n const authenticated = isAuthenticatedUserInfo(userInfo);\n\n req.authdog = {\n token,\n user: authenticated ? (userInfo.user ?? null) : null,\n isAuthenticated: authenticated,\n userInfo,\n };\n } catch {\n // A failed or untrusted userinfo lookup is treated as \"not authenticated\"\n // — never as a server error and never as an authenticated session.\n req.authdog = { token, user: null, isAuthenticated: false, userInfo: null };\n }\n\n return next();\n };\n};\n\n/**\n * Gate middleware that enforces authentication. Responds with `401` JSON when\n * the request has no valid Authdog session and calls `next()` otherwise.\n *\n * ⚠️ This is the real server-side enforcement point. Client-side checks are\n * presentational only; every protected route MUST sit behind `requireAuth`\n * (after `attachSession` has run) for the protection to be real.\n */\nexport const requireAuth: RequestHandler = (\n req: Request,\n res: Response,\n next: NextFunction,\n) => {\n if (!req.authdog?.isAuthenticated) {\n res.status(401).json({ error: \"Unauthorized\" });\n return;\n }\n next();\n};\n","import type { PublicKeyPayload } from \"@authdog/node-commons\";\nimport type { Request, RequestHandler, Response } from \"express\";\nimport { getPublicKeyPayload } from \"./commons\";\nimport { logoutHandler } from \"./logout\";\nimport { createAttachSession, requireAuth } from \"./middleware\";\nimport type { AttachSessionOptions, AuthdogConfig } from \"./types\";\n\n/** The instance returned by {@link createAuthdog}. */\nexport interface AuthdogServer {\n /**\n * Middleware that resolves the session and attaches `req.authdog`. Mount it\n * early (typically app-wide) so downstream handlers and `requireAuth` can\n * read the authentication context. Never throws or blocks the request.\n */\n attachSession: (options?: AttachSessionOptions) => RequestHandler;\n /**\n * Gate middleware that returns `401` for unauthenticated requests. This is\n * the real server-side enforcement point — place it before any protected\n * route. Requires `attachSession` to have run first.\n */\n requireAuth: RequestHandler;\n /** Handler that clears the session cookie and performs a safe redirect. */\n logout: (req: Request, res: Response) => void;\n /** The validated, parsed public-key payload (`environmentId`, `identityHost`, …). */\n getPublicKeyPayload: () => PublicKeyPayload;\n /** The validated public-key payload as a JSON string, e.g. to inline into HTML. */\n getPublicKey: () => string;\n}\n\n/**\n * Creates an Authdog server instance for Express.\n *\n * The public key is validated and parsed once here — enforcing the trusted\n * identity-host allowlist (SSRF / token-exfiltration protection) — so a\n * malformed or untrusted key fails fast at startup rather than on the first\n * request.\n *\n * ```ts\n * const authdog = createAuthdog({ publicKey: process.env.PK_AUTHDOG! });\n *\n * app.use(authdog.attachSession());\n * app.get(\"/me\", authdog.requireAuth, (req, res) => res.json(req.authdog!.user));\n * app.get(\"/logout\", authdog.logout);\n * ```\n */\nexport const createAuthdog = (config: AuthdogConfig): AuthdogServer => {\n if (!config.publicKey) {\n throw new Error(\"Public key is not defined\");\n }\n\n // Validate + parse eagerly so an invalid/untrusted key throws at startup.\n const payload = getPublicKeyPayload(config.publicKey);\n\n return {\n attachSession: (options?: AttachSessionOptions) =>\n createAttachSession(payload, options),\n requireAuth,\n logout: logoutHandler,\n getPublicKeyPayload: () => payload,\n getPublicKey: () => JSON.stringify(payload),\n };\n};\n"],"mappings":"AAAA,OACE,6BAAAA,MAEK,wBAUA,IAAMC,EAAuBC,GAC3BF,EAA0BE,CAAS,ECd5C,OAAS,wBAAAC,MAA4B,wBCArC,OAAS,gBAAAC,MAAoB,wBAItB,IAAMC,EAAsB,kBAYtBC,EAAmBC,GAAgC,CAE9D,IAAMC,EAAaD,EAAI,QAAQ,cAC/B,GAAIC,GAAc,cAAc,KAAKA,CAAU,EAAG,CAChD,IAAMC,EAAQD,EAAW,QAAQ,cAAe,EAAE,EAAE,KAAK,EACzD,GAAIC,EACF,OAAOA,CAEX,CAEA,IAAMC,EAAeH,EAAI,QAAQ,QAAU,KAC3C,OAAKG,EAIWN,EAAaM,CAAY,EAC1B,KAAMC,GAAMA,EAAE,OAASN,CAAmB,GAAG,OAAS,KAJ5D,IAKX,EDhBO,IAAMO,EAAgB,CAACC,EAAcC,IAAwB,CAClE,IAAMC,EAAS,QAAQ,IAAI,WAAa,aAAe,WAAa,GAEpED,EAAI,UACF,aACA,GAAGE,CAAmB,8DAA8DD,CAAM,eAC5F,EAEA,IAAME,EAAcC,EAAqBL,EAAI,MAAM,aAAc,GAAG,EAEpEC,EAAI,SAAS,IAAKG,CAAW,CAC/B,EE5BA,OACE,iBAAAE,EACA,2BAAAC,MAEK,wBAMP,IAAMC,EAAmC,CACvC,MAAO,KACP,KAAM,KACN,gBAAiB,GACjB,SAAU,IACZ,EAaaC,EAAsB,CACjCC,EACAC,EAAgC,CAAC,IACd,CACnB,IAAMC,EAAYD,EAAQ,WAAa,GAEvC,MAAO,OAAOE,EAAcC,EAAgBC,IAAuB,CACjE,IAAMC,EAAQC,EAAgBJ,CAAG,EAEjC,GAAI,CAACG,EACH,OAAAH,EAAI,QAAU,CAAE,GAAGL,CAAU,EACtBO,EAAK,EAMd,GAAI,CAACH,EACH,OAAAC,EAAI,QAAU,CACZ,MAAAG,EACA,KAAM,KACN,gBAAiB,GACjB,SAAU,IACZ,EACOD,EAAK,EAGd,GAAI,CACF,IAAMG,EAAW,MAAMC,EACrBT,EAAQ,aACRA,EAAQ,cACRM,CACF,EAEMI,EAAgBC,EAAwBH,CAAQ,EAEtDL,EAAI,QAAU,CACZ,MAAAG,EACA,KAAMI,EAAiBF,EAAS,MAAQ,KAAQ,KAChD,gBAAiBE,EACjB,SAAAF,CACF,CACF,MAAQ,CAGNL,EAAI,QAAU,CAAE,MAAAG,EAAO,KAAM,KAAM,gBAAiB,GAAO,SAAU,IAAK,CAC5E,CAEA,OAAOD,EAAK,CACd,CACF,EAUaO,EAA8B,CACzCT,EACAU,EACAR,IACG,CACH,GAAI,CAACF,EAAI,SAAS,gBAAiB,CACjCU,EAAI,OAAO,GAAG,EAAE,KAAK,CAAE,MAAO,cAAe,CAAC,EAC9C,MACF,CACAR,EAAK,CACP,ECrDO,IAAMS,EAAiBC,GAAyC,CACrE,GAAI,CAACA,EAAO,UACV,MAAM,IAAI,MAAM,2BAA2B,EAI7C,IAAMC,EAAUC,EAAoBF,EAAO,SAAS,EAEpD,MAAO,CACL,cAAgBG,GACdC,EAAoBH,EAASE,CAAO,EACtC,YAAAE,EACA,OAAQC,EACR,oBAAqB,IAAML,EAC3B,aAAc,IAAM,KAAK,UAAUA,CAAO,CAC5C,CACF","names":["validateAndParsePublicKey","getPublicKeyPayload","publicKey","sanitizeRedirectPath","parseCookies","SESSION_COOKIE_NAME","getSessionToken","req","authHeader","token","cookieHeader","c","logoutHandler","req","res","secure","SESSION_COOKIE_NAME","redirectUrl","sanitizeRedirectPath","fetchUserData","isAuthenticatedUserInfo","ANONYMOUS","createAttachSession","payload","options","fetchUser","req","_res","next","token","getSessionToken","userInfo","fetchUserData","authenticated","isAuthenticatedUserInfo","requireAuth","res","createAuthdog","config","payload","getPublicKeyPayload","options","createAttachSession","requireAuth","logoutHandler"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@authdog/express",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Authdog Express 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/express"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/authdog-labs/web-sdk/tree/main/packages/express#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
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"express": "^4.18.0 || ^5.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/express": "^5.0.0",
|
|
44
|
+
"@types/node": "^22.14.1",
|
|
45
|
+
"express": "^5.0.0",
|
|
46
|
+
"prettier": "^3.4.2",
|
|
47
|
+
"tsup": "^8.3.5",
|
|
48
|
+
"typescript": "^5.7.2",
|
|
49
|
+
"vitest": "^2.1.8"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"registry": "https://registry.npmjs.org/",
|
|
53
|
+
"access": "public"
|
|
54
|
+
}
|
|
55
|
+
}
|