@monotykamary/localterm-server 2.34.0 → 2.35.1
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/dist/cdp/cdp-client.d.ts +30 -0
- package/dist/cdp/cdp-client.d.ts.map +1 -1
- package/dist/cdp/cdp-client.js +80 -0
- package/dist/cdp/cdp-client.js.map +1 -1
- package/dist/constants.d.ts +17 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +27 -0
- package/dist/constants.js.map +1 -1
- package/dist/daemon-config-store.d.ts +2 -0
- package/dist/daemon-config-store.d.ts.map +1 -1
- package/dist/daemon-config-store.js +14 -1
- package/dist/daemon-config-store.js.map +1 -1
- package/dist/identity/credential-store.d.ts +18 -0
- package/dist/identity/credential-store.d.ts.map +1 -0
- package/dist/identity/credential-store.js +76 -0
- package/dist/identity/credential-store.js.map +1 -0
- package/dist/identity/factory.d.ts +3 -0
- package/dist/identity/factory.d.ts.map +1 -0
- package/dist/identity/factory.js +19 -0
- package/dist/identity/factory.js.map +1 -0
- package/dist/identity/header-provider.d.ts +3 -0
- package/dist/identity/header-provider.d.ts.map +1 -0
- package/dist/identity/header-provider.js +33 -0
- package/dist/identity/header-provider.js.map +1 -0
- package/dist/identity/oidc-provider.d.ts +4 -0
- package/dist/identity/oidc-provider.d.ts.map +1 -0
- package/dist/identity/oidc-provider.js +172 -0
- package/dist/identity/oidc-provider.js.map +1 -0
- package/dist/identity/passkey-provider.d.ts +3 -0
- package/dist/identity/passkey-provider.d.ts.map +1 -0
- package/dist/identity/passkey-provider.js +233 -0
- package/dist/identity/passkey-provider.js.map +1 -0
- package/dist/identity/proxy-allowlist.d.ts +5 -0
- package/dist/identity/proxy-allowlist.d.ts.map +1 -0
- package/dist/identity/proxy-allowlist.js +64 -0
- package/dist/identity/proxy-allowlist.js.map +1 -0
- package/dist/identity/resolve.d.ts +11 -0
- package/dist/identity/resolve.d.ts.map +1 -0
- package/dist/identity/resolve.js +57 -0
- package/dist/identity/resolve.js.map +1 -0
- package/dist/identity/session-cookie.d.ts +10 -0
- package/dist/identity/session-cookie.d.ts.map +1 -0
- package/dist/identity/session-cookie.js +92 -0
- package/dist/identity/session-cookie.js.map +1 -0
- package/dist/identity/types.d.ts +49 -0
- package/dist/identity/types.d.ts.map +1 -0
- package/dist/identity/types.js +2 -0
- package/dist/identity/types.js.map +1 -0
- package/dist/identity/user-store.d.ts +16 -0
- package/dist/identity/user-store.d.ts.map +1 -0
- package/dist/identity/user-store.js +77 -0
- package/dist/identity/user-store.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -31
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +2 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/schemas.d.ts +79 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +55 -1
- package/dist/schemas.js.map +1 -1
- package/dist/secret-store.d.ts.map +1 -1
- package/dist/secret-store.js +4 -1
- package/dist/secret-store.js.map +1 -1
- package/dist/session-automation.d.ts +7 -2
- package/dist/session-automation.d.ts.map +1 -1
- package/dist/session-automation.js +27 -8
- package/dist/session-automation.js.map +1 -1
- package/dist/session-manager.d.ts +20 -17
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +63 -44
- package/dist/session-manager.js.map +1 -1
- package/dist/utils/timing-safe-equal.d.ts +2 -0
- package/dist/utils/timing-safe-equal.d.ts.map +1 -0
- package/dist/utils/timing-safe-equal.js +12 -0
- package/dist/utils/timing-safe-equal.js.map +1 -0
- package/package.json +4 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { IDENTITY_HEADER_DEFAULT, IDENTITY_PROXY_DEFAULT, IDENTITY_USER_MAX_LENGTH, } from "../constants.js";
|
|
2
|
+
import { createProxyAllowlist } from "./proxy-allowlist.js";
|
|
3
|
+
// The identity provider that trusts a proxy-set header. Covers every external
|
|
4
|
+
// identity-aware proxy and self-hosted forward-auth with no in-app login flow:
|
|
5
|
+
// the proxy authenticates and forwards the user; localterm reads it.
|
|
6
|
+
//
|
|
7
|
+
// The header is only honored when the request's source IP is inside
|
|
8
|
+
// `trustedProxy` (default `"loopback"` — the common single-box deployment where
|
|
9
|
+
// the proxy runs on the same host as the daemon, so only loopback can reach it
|
|
10
|
+
// AND forge the header). A request from the proxy with no header resolves to
|
|
11
|
+
// the operator tier (no identity asserted): that's the CLI from loopback and
|
|
12
|
+
// the daemon's own CDP automation tabs, which keep full access — the admin
|
|
13
|
+
// parity a shared gateway needs. So `denyUnauthenticated` is false: a
|
|
14
|
+
// trusted-proxy request with no header is the operator, not a rejection.
|
|
15
|
+
export const createHeaderIdentityProvider = (config) => {
|
|
16
|
+
const header = config.header?.trim() || IDENTITY_HEADER_DEFAULT;
|
|
17
|
+
const allowlist = createProxyAllowlist(config.trustedProxy?.trim() || IDENTITY_PROXY_DEFAULT);
|
|
18
|
+
return {
|
|
19
|
+
kind: "header",
|
|
20
|
+
denyUnauthenticated: false,
|
|
21
|
+
operatorToken: null,
|
|
22
|
+
identify: (context, sourceIp) => {
|
|
23
|
+
const value = context.req.header(header);
|
|
24
|
+
if (!value)
|
|
25
|
+
return null;
|
|
26
|
+
if (!sourceIp || !allowlist.contains(sourceIp))
|
|
27
|
+
return null;
|
|
28
|
+
const user = value.trim().slice(0, IDENTITY_USER_MAX_LENGTH);
|
|
29
|
+
return user ? { user } : null;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=header-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"header-provider.js","sourceRoot":"","sources":["../../src/identity/header-provider.ts"],"names":[],"mappings":"AACA,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,oBAAoB,EAAuB,MAAM,sBAAsB,CAAC;AAEjF,8EAA8E;AAC9E,+EAA+E;AAC/E,qEAAqE;AACrE,EAAE;AACF,oEAAoE;AACpE,gFAAgF;AAChF,+EAA+E;AAC/E,6EAA6E;AAC7E,6EAA6E;AAC7E,2EAA2E;AAC3E,sEAAsE;AACtE,yEAAyE;AACzE,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,MAA4B,EAAoB,EAAE;IAC7F,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,uBAAuB,CAAC;IAChE,MAAM,SAAS,GAAmB,oBAAoB,CACpD,MAAM,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,sBAAsB,CACtD,CAAC;IACF,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,mBAAmB,EAAE,KAAK;QAC1B,aAAa,EAAE,IAAI;QACnB,QAAQ,EAAE,CAAC,OAAgB,EAAE,QAAuB,EAAmB,EAAE;YACvE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACzC,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAC;YACxB,IAAI,CAAC,QAAQ,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;gBAAE,OAAO,IAAI,CAAC;YAC5D,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,wBAAwB,CAAC,CAAC;YAC7D,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { IdentityProvider, IdentityProviderDeps, OidcIdentityConfig } from "./types.js";
|
|
2
|
+
export declare const sanitizeReturnTo: (value: string | null) => string;
|
|
3
|
+
export declare const createOidcIdentityProvider: (config: OidcIdentityConfig, deps: IdentityProviderDeps) => IdentityProvider;
|
|
4
|
+
//# sourceMappingURL=oidc-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc-provider.d.ts","sourceRoot":"","sources":["../../src/identity/oidc-provider.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAqCpB,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,GAAG,IAAI,KAAG,MAGvD,CAAC;AA4IF,eAAO,MAAM,0BAA0B,GACrC,QAAQ,kBAAkB,EAC1B,MAAM,oBAAoB,KACzB,gBA6CF,CAAC"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import * as oauth from "oauth4webapi";
|
|
3
|
+
import { AUTH_STATE_TTL_MS, IDENTITY_USER_MAX_LENGTH } from "../constants.js";
|
|
4
|
+
import { clearSessionCookie, readSessionIdentity, setSessionCookie } from "./session-cookie.js";
|
|
5
|
+
class OidcStateStore {
|
|
6
|
+
states = new Map();
|
|
7
|
+
set(state, entry) {
|
|
8
|
+
this.states.set(state, entry);
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
for (const [key, value] of this.states) {
|
|
11
|
+
if (value.expiresAt < now)
|
|
12
|
+
this.states.delete(key);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
consume(state) {
|
|
16
|
+
const entry = this.states.get(state);
|
|
17
|
+
this.states.delete(state);
|
|
18
|
+
if (!entry || entry.expiresAt < Date.now())
|
|
19
|
+
return null;
|
|
20
|
+
return entry;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Only allow same-origin relative paths as the post-login landing target, to
|
|
24
|
+
// keep the callback from being an open redirect: a value must start with `/`
|
|
25
|
+
// and not `//` (a protocol-relative URL the browser would treat as absolute).
|
|
26
|
+
export const sanitizeReturnTo = (value) => {
|
|
27
|
+
if (!value || !value.startsWith("/") || value.startsWith("//"))
|
|
28
|
+
return "/";
|
|
29
|
+
return value;
|
|
30
|
+
};
|
|
31
|
+
const buildRedirectUri = (origin) => `${origin.replace(/\/$/, "")}/auth/oidc/callback`;
|
|
32
|
+
const buildOidcRoutes = (deps) => {
|
|
33
|
+
const app = new Hono();
|
|
34
|
+
// GET /oidc/login?returnTo=<path> — kick off the auth-code flow: mint PKCE +
|
|
35
|
+
// state + nonce, remember them against `state`, 302 to the IdP's authz
|
|
36
|
+
// endpoint. The `redirect_uri` is the daemon's announced origin + this
|
|
37
|
+
// callback path, which must be registered with the IdP — so OIDC needs a
|
|
38
|
+
// stable announced origin (the tailnet/local-https surface), unlike passkey
|
|
39
|
+
// which binds to whatever origin the browser is on.
|
|
40
|
+
app.get("/oidc/login", async (context) => {
|
|
41
|
+
const origin = deps.getOrigin();
|
|
42
|
+
if (!origin)
|
|
43
|
+
return context.json({ error: "no_origin" }, 500);
|
|
44
|
+
let metadata;
|
|
45
|
+
try {
|
|
46
|
+
metadata = await deps.getMetadata();
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return context.json({ error: "issuer_unreachable" }, 502);
|
|
50
|
+
}
|
|
51
|
+
if (!metadata.authorization_endpoint) {
|
|
52
|
+
return context.json({ error: "issuer_unsupported" }, 502);
|
|
53
|
+
}
|
|
54
|
+
const redirectUri = buildRedirectUri(origin);
|
|
55
|
+
const codeVerifier = oauth.generateRandomCodeVerifier();
|
|
56
|
+
const codeChallenge = await oauth.calculatePKCECodeChallenge(codeVerifier);
|
|
57
|
+
const state = oauth.generateRandomState();
|
|
58
|
+
const nonce = oauth.generateRandomNonce();
|
|
59
|
+
const returnTo = sanitizeReturnTo(new URL(context.req.url).searchParams.get("returnTo"));
|
|
60
|
+
deps.stateStore.set(state, {
|
|
61
|
+
codeVerifier,
|
|
62
|
+
nonce,
|
|
63
|
+
returnTo,
|
|
64
|
+
expiresAt: Date.now() + AUTH_STATE_TTL_MS,
|
|
65
|
+
});
|
|
66
|
+
const params = new URLSearchParams();
|
|
67
|
+
params.set("client_id", deps.client.client_id);
|
|
68
|
+
params.set("redirect_uri", redirectUri);
|
|
69
|
+
params.set("response_type", "code");
|
|
70
|
+
params.set("scope", deps.scope);
|
|
71
|
+
params.set("state", state);
|
|
72
|
+
params.set("nonce", nonce);
|
|
73
|
+
params.set("code_challenge", codeChallenge);
|
|
74
|
+
params.set("code_challenge_method", "S256");
|
|
75
|
+
const authUrl = new URL(metadata.authorization_endpoint);
|
|
76
|
+
for (const [key, value] of params.entries()) {
|
|
77
|
+
authUrl.searchParams.set(key, value);
|
|
78
|
+
}
|
|
79
|
+
return context.redirect(authUrl.toString(), 302);
|
|
80
|
+
});
|
|
81
|
+
// GET /oidc/callback?code=&state= — the IdP redirects here. Consume the
|
|
82
|
+
// state, validate the response, exchange the code for tokens (verifying the
|
|
83
|
+
// ID-token nonce), fetch userinfo, and issue a session cookie for the
|
|
84
|
+
// configured claim (default email, falling back to `sub`). Any failure
|
|
85
|
+
// redirects to `/` rather than leaking an error page to the browser.
|
|
86
|
+
app.get("/oidc/callback", async (context) => {
|
|
87
|
+
const origin = deps.getOrigin();
|
|
88
|
+
if (!origin)
|
|
89
|
+
return context.redirect("/", 302);
|
|
90
|
+
const callbackUrl = new URL(context.req.url);
|
|
91
|
+
const state = callbackUrl.searchParams.get("state");
|
|
92
|
+
if (!state)
|
|
93
|
+
return context.redirect("/", 302);
|
|
94
|
+
const stored = deps.stateStore.consume(state);
|
|
95
|
+
if (!stored)
|
|
96
|
+
return context.redirect("/", 302);
|
|
97
|
+
try {
|
|
98
|
+
const metadata = await deps.getMetadata();
|
|
99
|
+
const validated = oauth.validateAuthResponse(metadata, deps.client, callbackUrl.searchParams, state);
|
|
100
|
+
const tokenResponse = await oauth.authorizationCodeGrantRequest(metadata, deps.client, deps.clientAuth, validated, buildRedirectUri(origin), stored.codeVerifier);
|
|
101
|
+
const tokens = await oauth.processAuthorizationCodeResponse(metadata, deps.client, tokenResponse, {
|
|
102
|
+
expectedNonce: stored.nonce,
|
|
103
|
+
});
|
|
104
|
+
const userInfo = await oauth.processUserInfoResponse(metadata, deps.client, oauth.skipSubjectCheck, await oauth.userInfoRequest(metadata, deps.client, tokens.access_token));
|
|
105
|
+
const raw = userInfo[deps.claim];
|
|
106
|
+
const user = (typeof raw === "string" ? raw : userInfo.sub).slice(0, IDENTITY_USER_MAX_LENGTH);
|
|
107
|
+
setSessionCookie(context, deps.secret, user);
|
|
108
|
+
return context.redirect(stored.returnTo, 302);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return context.redirect("/", 302);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
app.post("/oidc/logout", (context) => {
|
|
115
|
+
clearSessionCookie(context);
|
|
116
|
+
return context.json({ ok: true });
|
|
117
|
+
});
|
|
118
|
+
app.get("/oidc/me", (context) => {
|
|
119
|
+
const identity = readSessionIdentity(context, deps.secret);
|
|
120
|
+
return context.json({ user: identity?.user ?? null });
|
|
121
|
+
});
|
|
122
|
+
return app;
|
|
123
|
+
};
|
|
124
|
+
// The bring-your-own-IdP provider: any OIDC IdP (Google, GitHub, or self-hosted
|
|
125
|
+
// Authentik/Zitadel/Keycloak) authenticates via an authorization-code + PKCE
|
|
126
|
+
// flow; localterm keeps no passwords. Like `passkey`, `identify` reads the
|
|
127
|
+
// signed session cookie the callback issued and `denyUnauthenticated` is true
|
|
128
|
+
// (the gate rejects a no-session request). Discovery is cached lazily and
|
|
129
|
+
// retried on failure; the `redirect_uri` is the daemon's announced origin.
|
|
130
|
+
export const createOidcIdentityProvider = (config, deps) => {
|
|
131
|
+
const issuerUrl = new URL(config.issuer);
|
|
132
|
+
const client = { client_id: config.clientId };
|
|
133
|
+
const clientAuth = config.clientSecret
|
|
134
|
+
? oauth.ClientSecretPost(config.clientSecret)
|
|
135
|
+
: oauth.None();
|
|
136
|
+
const claim = config.claim ?? "email";
|
|
137
|
+
const scope = config.scope ?? "openid email";
|
|
138
|
+
const stateStore = new OidcStateStore();
|
|
139
|
+
const secret = deps.secret;
|
|
140
|
+
// Cached OIDC discovery (the IdP's metadata). Resolved once, shared across
|
|
141
|
+
// flows; reset to null on failure so the next attempt re-discovers rather
|
|
142
|
+
// than caching a bad result. A single shared promise avoids duplicate
|
|
143
|
+
// concurrent discoveries.
|
|
144
|
+
let metadataPromise = null;
|
|
145
|
+
const getMetadata = () => {
|
|
146
|
+
if (!metadataPromise) {
|
|
147
|
+
metadataPromise = (async () => oauth.processDiscoveryResponse(issuerUrl, await oauth.discoveryRequest(issuerUrl)))();
|
|
148
|
+
metadataPromise.catch(() => {
|
|
149
|
+
metadataPromise = null;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return metadataPromise;
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
kind: "oidc",
|
|
156
|
+
denyUnauthenticated: true,
|
|
157
|
+
operatorToken: config.operatorToken ?? null,
|
|
158
|
+
identify: (context) => readSessionIdentity(context, secret),
|
|
159
|
+
routes: () => buildOidcRoutes({
|
|
160
|
+
issuerUrl,
|
|
161
|
+
client,
|
|
162
|
+
clientAuth,
|
|
163
|
+
claim,
|
|
164
|
+
scope,
|
|
165
|
+
getOrigin: deps.getOrigin,
|
|
166
|
+
stateStore,
|
|
167
|
+
secret,
|
|
168
|
+
getMetadata,
|
|
169
|
+
}),
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=oidc-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc-provider.js","sourceRoot":"","sources":["../../src/identity/oidc-provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAO9E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAchG,MAAM,cAAc;IACD,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAC;IAEvD,GAAG,CAAC,KAAa,EAAE,KAAgB;QACjC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACvC,IAAI,KAAK,CAAC,SAAS,GAAG,GAAG;gBAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,OAAO,CAAC,KAAa;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE;YAAE,OAAO,IAAI,CAAC;QACxD,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AAED,6EAA6E;AAC7E,6EAA6E;AAC7E,8EAA8E;AAC9E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAoB,EAAU,EAAE;IAC/D,IAAI,CAAC,KAAK,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,CAAC;IAC3E,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,gBAAgB,GAAG,CAAC,MAAc,EAAU,EAAE,CAClD,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,qBAAqB,CAAC;AAcpD,MAAM,eAAe,GAAG,CAAC,IAAmB,EAAQ,EAAE;IACpD,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IAEvB,6EAA6E;IAC7E,uEAAuE;IACvE,uEAAuE;IACvE,yEAAyE;IACzE,4EAA4E;IAC5E,oDAAoD;IACpD,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAChC,IAAI,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,GAAG,CAAC,CAAC;QAC9D,IAAI,QAA6B,CAAC;QAClC,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,EAAE,GAAG,CAAC,CAAC;QAC5D,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,sBAAsB,EAAE,CAAC;YACrC,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,EAAE,GAAG,CAAC,CAAC;QAC5D,CAAC;QACD,MAAM,WAAW,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAC7C,MAAM,YAAY,GAAG,KAAK,CAAC,0BAA0B,EAAE,CAAC;QACxD,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,0BAA0B,CAAC,YAAY,CAAC,CAAC;QAC3E,MAAM,KAAK,GAAG,KAAK,CAAC,mBAAmB,EAAE,CAAC;QAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,mBAAmB,EAAE,CAAC;QAC1C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;QACzF,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE;YACzB,YAAY;YACZ,KAAK;YACL,QAAQ;YACR,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,iBAAiB;SAC1C,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC/C,MAAM,CAAC,GAAG,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;QACxC,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;QACpC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC3B,MAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,aAAa,CAAC,CAAC;QAC5C,MAAM,CAAC,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC;QAC5C,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAC;QACzD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;YAC5C,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACvC,CAAC;QACD,OAAO,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,wEAAwE;IACxE,4EAA4E;IAC5E,sEAAsE;IACtE,uEAAuE;IACvE,qEAAqE;IACrE,GAAG,CAAC,GAAG,CAAC,gBAAgB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QAChC,IAAI,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC/C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,WAAW,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACpD,IAAI,CAAC,KAAK;YAAE,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QAC/C,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YAC1C,MAAM,SAAS,GAAG,KAAK,CAAC,oBAAoB,CAC1C,QAAQ,EACR,IAAI,CAAC,MAAM,EACX,WAAW,CAAC,YAAY,EACxB,KAAK,CACN,CAAC;YACF,MAAM,aAAa,GAAG,MAAM,KAAK,CAAC,6BAA6B,CAC7D,QAAQ,EACR,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,UAAU,EACf,SAAS,EACT,gBAAgB,CAAC,MAAM,CAAC,EACxB,MAAM,CAAC,YAAY,CACpB,CAAC;YACF,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,gCAAgC,CACzD,QAAQ,EACR,IAAI,CAAC,MAAM,EACX,aAAa,EACb;gBACE,aAAa,EAAE,MAAM,CAAC,KAAK;aAC5B,CACF,CAAC;YACF,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,uBAAuB,CAClD,QAAQ,EACR,IAAI,CAAC,MAAM,EACX,KAAK,CAAC,gBAAgB,EACtB,MAAM,KAAK,CAAC,eAAe,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,CACxE,CAAC;YACF,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACjC,MAAM,IAAI,GAAG,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,CAC/D,CAAC,EACD,wBAAwB,CACzB,CAAC;YACF,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAC7C,OAAO,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACpC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,OAAO,EAAE,EAAE;QACnC,kBAAkB,CAAC,OAAO,CAAC,CAAC;QAC5B,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,EAAE;QAC9B,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC3D,OAAO,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF,gFAAgF;AAChF,6EAA6E;AAC7E,2EAA2E;AAC3E,8EAA8E;AAC9E,0EAA0E;AAC1E,2EAA2E;AAC3E,MAAM,CAAC,MAAM,0BAA0B,GAAG,CACxC,MAA0B,EAC1B,IAA0B,EACR,EAAE;IACpB,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,MAAM,GAAW,EAAE,SAAS,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;IACtD,MAAM,UAAU,GAAG,MAAM,CAAC,YAAY;QACpC,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,YAAY,CAAC;QAC7C,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACjB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,OAAO,CAAC;IACtC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,cAAc,CAAC;IAC7C,MAAM,UAAU,GAAG,IAAI,cAAc,EAAE,CAAC;IACxC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAE3B,2EAA2E;IAC3E,0EAA0E;IAC1E,sEAAsE;IACtE,0BAA0B;IAC1B,IAAI,eAAe,GAAwC,IAAI,CAAC;IAChE,MAAM,WAAW,GAAG,GAAiC,EAAE;QACrD,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,eAAe,GAAG,CAAC,KAAK,IAAI,EAAE,CAC5B,KAAK,CAAC,wBAAwB,CAAC,SAAS,EAAE,MAAM,KAAK,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;YACxF,eAAe,CAAC,KAAK,CAAC,GAAG,EAAE;gBACzB,eAAe,GAAG,IAAI,CAAC;YACzB,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,eAAe,CAAC;IACzB,CAAC,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,MAAM;QACZ,mBAAmB,EAAE,IAAI;QACzB,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,IAAI;QAC3C,QAAQ,EAAE,CAAC,OAAgB,EAAmB,EAAE,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC;QACrF,MAAM,EAAE,GAAG,EAAE,CACX,eAAe,CAAC;YACd,SAAS;YACT,MAAM;YACN,UAAU;YACV,KAAK;YACL,KAAK;YACL,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,UAAU;YACV,MAAM;YACN,WAAW;SACZ,CAAC;KACL,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { IdentityProvider, IdentityProviderDeps, PasskeyIdentityConfig } from "./types.js";
|
|
2
|
+
export declare const createPasskeyIdentityProvider: (config: PasskeyIdentityConfig, deps: IdentityProviderDeps) => IdentityProvider;
|
|
3
|
+
//# sourceMappingURL=passkey-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey-provider.d.ts","sourceRoot":"","sources":["../../src/identity/passkey-provider.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAEV,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AA0OpB,eAAO,MAAM,6BAA6B,GACxC,QAAQ,qBAAqB,EAC7B,MAAM,oBAAoB,KACzB,gBAwBF,CAAC"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from "@simplewebauthn/server";
|
|
4
|
+
import { AUTH_CHALLENGE_TTL_MS, HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_FORBIDDEN, IDENTITY_RP_NAME_DEFAULT, IDENTITY_USERNAME_MAX_LENGTH, IDENTITY_USERNAME_MIN_LENGTH, } from "../constants.js";
|
|
5
|
+
import { CredentialStore } from "./credential-store.js";
|
|
6
|
+
import { UserStore } from "./user-store.js";
|
|
7
|
+
import { clearSessionCookie, readSessionIdentity, setSessionCookie } from "./session-cookie.js";
|
|
8
|
+
class ChallengeStore {
|
|
9
|
+
challenges = new Map();
|
|
10
|
+
set(challenge, kind) {
|
|
11
|
+
this.challenges.set(challenge, { kind, expiresAt: Date.now() + AUTH_CHALLENGE_TTL_MS });
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [key, entry] of this.challenges) {
|
|
14
|
+
if (entry.expiresAt < now)
|
|
15
|
+
this.challenges.delete(key);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Single-use: delete on read, return true only if it matched the expected
|
|
19
|
+
// kind and hadn't expired. A register challenge can't satisfy a login verify
|
|
20
|
+
// (and vice versa), and a consumed challenge can't be replayed.
|
|
21
|
+
consume(challenge, kind) {
|
|
22
|
+
const entry = this.challenges.get(challenge);
|
|
23
|
+
this.challenges.delete(challenge);
|
|
24
|
+
return entry?.kind === kind && entry.expiresAt >= Date.now();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
const readBody = async (context) => {
|
|
29
|
+
try {
|
|
30
|
+
const json = await context.req.json();
|
|
31
|
+
return isObject(json) ? json : {};
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const normalizeUsername = (value) => {
|
|
38
|
+
if (typeof value !== "string")
|
|
39
|
+
return null;
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
if (trimmed.length < IDENTITY_USERNAME_MIN_LENGTH ||
|
|
42
|
+
trimmed.length > IDENTITY_USERNAME_MAX_LENGTH) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return trimmed;
|
|
46
|
+
};
|
|
47
|
+
// The RP origin/id come from the browser's own Origin header (the surface the
|
|
48
|
+
// user is actually on), falling back to the daemon's announced origin. A
|
|
49
|
+
// passkey is bound to the RP ID (hostname), so this is also why a passkey
|
|
50
|
+
// registered on the loopback origin won't work on the tailnet origin and
|
|
51
|
+
// vice-versa — inherent to WebAuthn, surfaced here as expectedOrigin/RPID.
|
|
52
|
+
const resolveRp = (context, getOrigin) => {
|
|
53
|
+
const raw = context.req.header("origin") || getOrigin();
|
|
54
|
+
if (!raw)
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
const url = new URL(raw);
|
|
58
|
+
if (!url.hostname)
|
|
59
|
+
return null;
|
|
60
|
+
return { origin: url.origin, rpID: url.hostname };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
// Quick structural reject for the credential response the browser sends; the
|
|
67
|
+
// real validation is simplewebauthn's verify (which throws on malformed input,
|
|
68
|
+
// caught by the route). The predicate narrows to the library's exact type so
|
|
69
|
+
// no cast is needed at the verify call.
|
|
70
|
+
const isRegistrationResponse = (value) => isObject(value) && typeof value.id === "string" && isObject(value.response);
|
|
71
|
+
const isAuthenticationResponse = (value) => isObject(value) && typeof value.id === "string" && isObject(value.response);
|
|
72
|
+
const buildPasskeyRoutes = (deps) => {
|
|
73
|
+
const app = new Hono();
|
|
74
|
+
app.get("/passkey/me", (context) => {
|
|
75
|
+
const identity = readSessionIdentity(context, deps.secret);
|
|
76
|
+
return context.json({ user: identity?.user ?? null });
|
|
77
|
+
});
|
|
78
|
+
app.post("/passkey/register/options", async (context) => {
|
|
79
|
+
if (!deps.registrationOpen) {
|
|
80
|
+
return context.json({ error: "registration_closed" }, HTTP_STATUS_FORBIDDEN);
|
|
81
|
+
}
|
|
82
|
+
const body = await readBody(context);
|
|
83
|
+
const username = normalizeUsername(body.username);
|
|
84
|
+
if (!username)
|
|
85
|
+
return context.json({ error: "invalid_username" }, HTTP_STATUS_BAD_REQUEST);
|
|
86
|
+
const rp = resolveRp(context, deps.getOrigin);
|
|
87
|
+
if (!rp)
|
|
88
|
+
return context.json({ error: "invalid_origin" }, HTTP_STATUS_BAD_REQUEST);
|
|
89
|
+
const excludeCredentials = (deps.userStore.get(username)?.credentialIds ?? []).map((id) => ({
|
|
90
|
+
id,
|
|
91
|
+
}));
|
|
92
|
+
const options = await generateRegistrationOptions({
|
|
93
|
+
rpName: deps.rpName,
|
|
94
|
+
rpID: rp.rpID,
|
|
95
|
+
userName: username,
|
|
96
|
+
excludeCredentials,
|
|
97
|
+
authenticatorSelection: { residentKey: "preferred", userVerification: "preferred" },
|
|
98
|
+
});
|
|
99
|
+
deps.challenges.set(options.challenge, "register");
|
|
100
|
+
return context.json(options);
|
|
101
|
+
});
|
|
102
|
+
app.post("/passkey/register/verify", async (context) => {
|
|
103
|
+
if (!deps.registrationOpen) {
|
|
104
|
+
return context.json({ error: "registration_closed" }, HTTP_STATUS_FORBIDDEN);
|
|
105
|
+
}
|
|
106
|
+
const body = await readBody(context);
|
|
107
|
+
const username = normalizeUsername(body.username);
|
|
108
|
+
if (!username || !isRegistrationResponse(body.response)) {
|
|
109
|
+
return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
|
|
110
|
+
}
|
|
111
|
+
const rp = resolveRp(context, deps.getOrigin);
|
|
112
|
+
if (!rp)
|
|
113
|
+
return context.json({ error: "invalid_origin" }, HTTP_STATUS_BAD_REQUEST);
|
|
114
|
+
const response = body.response;
|
|
115
|
+
let verified;
|
|
116
|
+
try {
|
|
117
|
+
verified = await verifyRegistrationResponse({
|
|
118
|
+
response,
|
|
119
|
+
expectedChallenge: (challenge) => deps.challenges.consume(challenge, "register"),
|
|
120
|
+
expectedOrigin: rp.origin,
|
|
121
|
+
expectedRPID: rp.rpID,
|
|
122
|
+
requireUserVerification: true,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return context.json({ error: "verification_failed" }, HTTP_STATUS_BAD_REQUEST);
|
|
127
|
+
}
|
|
128
|
+
if (!verified || !verified.verified || !verified.registrationInfo) {
|
|
129
|
+
return context.json({ error: "verification_failed" }, HTTP_STATUS_BAD_REQUEST);
|
|
130
|
+
}
|
|
131
|
+
const credential = verified.registrationInfo.credential;
|
|
132
|
+
deps.userStore.findOrCreate(username);
|
|
133
|
+
deps.userStore.addCredential(username, credential.id);
|
|
134
|
+
deps.credentialStore.put({
|
|
135
|
+
id: credential.id,
|
|
136
|
+
publicKey: Buffer.from(credential.publicKey).toString("base64"),
|
|
137
|
+
counter: credential.counter,
|
|
138
|
+
username,
|
|
139
|
+
});
|
|
140
|
+
setSessionCookie(context, deps.secret, username);
|
|
141
|
+
return context.json({ user: username });
|
|
142
|
+
});
|
|
143
|
+
app.post("/passkey/login/options", async (context) => {
|
|
144
|
+
const body = await readBody(context);
|
|
145
|
+
const username = normalizeUsername(body.username);
|
|
146
|
+
const rp = resolveRp(context, deps.getOrigin);
|
|
147
|
+
if (!rp)
|
|
148
|
+
return context.json({ error: "invalid_origin" }, HTTP_STATUS_BAD_REQUEST);
|
|
149
|
+
const allowCredentials = username
|
|
150
|
+
? (deps.userStore.get(username)?.credentialIds ?? []).map((id) => ({ id }))
|
|
151
|
+
: undefined;
|
|
152
|
+
const options = await generateAuthenticationOptions({
|
|
153
|
+
rpID: rp.rpID,
|
|
154
|
+
allowCredentials,
|
|
155
|
+
userVerification: "preferred",
|
|
156
|
+
});
|
|
157
|
+
deps.challenges.set(options.challenge, "login");
|
|
158
|
+
return context.json(options);
|
|
159
|
+
});
|
|
160
|
+
app.post("/passkey/login/verify", async (context) => {
|
|
161
|
+
const body = await readBody(context);
|
|
162
|
+
if (!isAuthenticationResponse(body.response)) {
|
|
163
|
+
return context.json({ error: "invalid_body" }, HTTP_STATUS_BAD_REQUEST);
|
|
164
|
+
}
|
|
165
|
+
const response = body.response;
|
|
166
|
+
const rp = resolveRp(context, deps.getOrigin);
|
|
167
|
+
if (!rp)
|
|
168
|
+
return context.json({ error: "invalid_origin" }, HTTP_STATUS_BAD_REQUEST);
|
|
169
|
+
const stored = deps.credentialStore.get(response.id);
|
|
170
|
+
if (!stored)
|
|
171
|
+
return context.json({ error: "unknown_credential" }, HTTP_STATUS_BAD_REQUEST);
|
|
172
|
+
const credential = {
|
|
173
|
+
id: stored.id,
|
|
174
|
+
publicKey: Buffer.from(stored.publicKey, "base64"),
|
|
175
|
+
counter: stored.counter,
|
|
176
|
+
};
|
|
177
|
+
let verified;
|
|
178
|
+
try {
|
|
179
|
+
verified = await verifyAuthenticationResponse({
|
|
180
|
+
response,
|
|
181
|
+
expectedChallenge: (challenge) => deps.challenges.consume(challenge, "login"),
|
|
182
|
+
expectedOrigin: rp.origin,
|
|
183
|
+
expectedRPID: rp.rpID,
|
|
184
|
+
credential,
|
|
185
|
+
requireUserVerification: true,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return context.json({ error: "verification_failed" }, HTTP_STATUS_BAD_REQUEST);
|
|
190
|
+
}
|
|
191
|
+
if (!verified || !verified.verified) {
|
|
192
|
+
return context.json({ error: "verification_failed" }, HTTP_STATUS_BAD_REQUEST);
|
|
193
|
+
}
|
|
194
|
+
deps.credentialStore.updateCounter(stored.id, verified.authenticationInfo.newCounter);
|
|
195
|
+
setSessionCookie(context, deps.secret, stored.username);
|
|
196
|
+
return context.json({ user: stored.username });
|
|
197
|
+
});
|
|
198
|
+
app.post("/passkey/logout", (context) => {
|
|
199
|
+
clearSessionCookie(context);
|
|
200
|
+
return context.json({ ok: true });
|
|
201
|
+
});
|
|
202
|
+
return app;
|
|
203
|
+
};
|
|
204
|
+
// The self-contained identity provider: localterm is the identity authority.
|
|
205
|
+
// `identify` reads the signed session cookie the register/login flow set;
|
|
206
|
+
// `denyUnauthenticated: true` makes the gate reject any request without a
|
|
207
|
+
// valid session (401 / WS policy-violation) — unlike `header`, there's no
|
|
208
|
+
// operator fallback, because there's no external proxy to vouch for one.
|
|
209
|
+
// `routes()` is the `/auth/passkey/*` login flow mounted by the daemon.
|
|
210
|
+
export const createPasskeyIdentityProvider = (config, deps) => {
|
|
211
|
+
const rpName = config.rpName?.trim() || IDENTITY_RP_NAME_DEFAULT;
|
|
212
|
+
const registrationOpen = (config.registration ?? "open") === "open";
|
|
213
|
+
const userStore = new UserStore(path.join(deps.stateDirectory, "users.json"));
|
|
214
|
+
const credentialStore = new CredentialStore(path.join(deps.stateDirectory, "credentials.json"));
|
|
215
|
+
const challenges = new ChallengeStore();
|
|
216
|
+
const secret = deps.secret;
|
|
217
|
+
return {
|
|
218
|
+
kind: "passkey",
|
|
219
|
+
denyUnauthenticated: true,
|
|
220
|
+
operatorToken: config.operatorToken ?? null,
|
|
221
|
+
identify: (context) => readSessionIdentity(context, secret),
|
|
222
|
+
routes: () => buildPasskeyRoutes({
|
|
223
|
+
rpName,
|
|
224
|
+
registrationOpen,
|
|
225
|
+
getOrigin: deps.getOrigin,
|
|
226
|
+
userStore,
|
|
227
|
+
credentialStore,
|
|
228
|
+
challenges,
|
|
229
|
+
secret,
|
|
230
|
+
}),
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
//# sourceMappingURL=passkey-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"passkey-provider.js","sourceRoot":"","sources":["../../src/identity/passkey-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-allowlist.d.ts","sourceRoot":"","sources":["../../src/identity/proxy-allowlist.ts"],"names":[],"mappings":"AA2BA,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;CACnC;AAQD,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,cA6BnD,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
const IPV4_MAPPED_PREFIX = "::ffff:";
|
|
3
|
+
// net.BlockList checks family-strict, so a dual-stack listener hands us an
|
|
4
|
+
// IPv4-mapped IPv6 address (::ffff:1.2.3.4) that an IPv4 subnet rule would
|
|
5
|
+
// miss. Normalize it back to the v4 form so one allowlist rule covers both.
|
|
6
|
+
const normalizeIp = (ip) => {
|
|
7
|
+
if (ip.startsWith(IPV4_MAPPED_PREFIX)) {
|
|
8
|
+
return { address: ip.slice(IPV4_MAPPED_PREFIX.length), family: "ipv4" };
|
|
9
|
+
}
|
|
10
|
+
return { address: ip, family: ip.includes(":") ? "ipv6" : "ipv4" };
|
|
11
|
+
};
|
|
12
|
+
const parseCidr = (cidr) => {
|
|
13
|
+
const slash = cidr.indexOf("/");
|
|
14
|
+
if (slash === -1)
|
|
15
|
+
return null;
|
|
16
|
+
const address = cidr.slice(0, slash);
|
|
17
|
+
const prefix = Number.parseInt(cidr.slice(slash + 1), 10);
|
|
18
|
+
if (!Number.isInteger(prefix) || prefix < 0)
|
|
19
|
+
return null;
|
|
20
|
+
const family = address.includes(":") ? "ipv6" : "ipv4";
|
|
21
|
+
if (prefix > (family === "ipv4" ? 32 : 128))
|
|
22
|
+
return null;
|
|
23
|
+
return { address, prefix, family };
|
|
24
|
+
};
|
|
25
|
+
// Build a source-IP allowlist for the `header` identity provider. `spec` is one
|
|
26
|
+
// of the shorthands `"loopback"` (127/8, ::1) or `"private"` (RFC1918, CGNAT,
|
|
27
|
+
// link-local, ULA — mirrors the network-policy private check), a CIDR string
|
|
28
|
+
// (`"10.0.0.0/8"`, `"::1/128"`), or a bare address. The provider only honors
|
|
29
|
+
// the identity header when the request's source IP is in this range, so a
|
|
30
|
+
// direct caller forging the header from outside the proxy is ignored.
|
|
31
|
+
export const createProxyAllowlist = (spec) => {
|
|
32
|
+
const list = new net.BlockList();
|
|
33
|
+
if (spec === "loopback") {
|
|
34
|
+
list.addSubnet("127.0.0.0", 8, "ipv4");
|
|
35
|
+
list.addAddress("::1", "ipv6");
|
|
36
|
+
}
|
|
37
|
+
else if (spec === "private") {
|
|
38
|
+
list.addSubnet("127.0.0.0", 8, "ipv4");
|
|
39
|
+
list.addSubnet("10.0.0.0", 8, "ipv4");
|
|
40
|
+
list.addSubnet("172.16.0.0", 12, "ipv4");
|
|
41
|
+
list.addSubnet("192.168.0.0", 16, "ipv4");
|
|
42
|
+
list.addSubnet("100.64.0.0", 10, "ipv4");
|
|
43
|
+
list.addSubnet("169.254.0.0", 16, "ipv4");
|
|
44
|
+
list.addAddress("::1", "ipv6");
|
|
45
|
+
list.addSubnet("fc00::", 7, "ipv6");
|
|
46
|
+
list.addSubnet("fe80::", 10, "ipv6");
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const cidr = parseCidr(spec);
|
|
50
|
+
if (cidr) {
|
|
51
|
+
list.addSubnet(cidr.address, cidr.prefix, cidr.family);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
list.addAddress(spec, spec.includes(":") ? "ipv6" : "ipv4");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
contains: (ip) => {
|
|
59
|
+
const { address, family } = normalizeIp(ip);
|
|
60
|
+
return list.check(address, family);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=proxy-allowlist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-allowlist.js","sourceRoot":"","sources":["../../src/identity/proxy-allowlist.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import type { MiddlewareHandler } from "hono";
|
|
3
|
+
import type { Identity, IdentityProvider, SessionOwner } from "./types.js";
|
|
4
|
+
export declare const getRequestSourceIp: (context: Context) => string | null;
|
|
5
|
+
export interface IdentityResolver {
|
|
6
|
+
resolve: (context: Context, sourceIp?: string | null) => Identity | null;
|
|
7
|
+
}
|
|
8
|
+
export declare const createIdentityResolver: (provider: IdentityProvider | null) => IdentityResolver;
|
|
9
|
+
export declare const toSessionOwner: (identity: Identity | null) => SessionOwner;
|
|
10
|
+
export declare const createAuthGateMiddleware: (provider: IdentityProvider | null, resolveIdentity: (context: Context, sourceIp?: string | null) => Identity | null) => MiddlewareHandler;
|
|
11
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/identity/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAI9C,OAAO,KAAK,EAAE,QAAQ,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAO3E,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,KAAG,MAAM,GAAG,IAO9D,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,QAAQ,GAAG,IAAI,CAAC;CAC1E;AASD,eAAO,MAAM,sBAAsB,GAAI,UAAU,gBAAgB,GAAG,IAAI,KAAG,gBAGzE,CAAC;AAEH,eAAO,MAAM,cAAc,GAAI,UAAU,QAAQ,GAAG,IAAI,KAAG,YAC1B,CAAC;AAYlC,eAAO,MAAM,wBAAwB,GAEjC,UAAU,gBAAgB,GAAG,IAAI,EACjC,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,QAAQ,GAAG,IAAI,KAC/E,iBAkBF,CAAC"}
|