@nwire/auth-logto 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/__tests__/auth-logto.test.d.ts +16 -0
- package/dist/__tests__/auth-logto.test.d.ts.map +1 -0
- package/dist/__tests__/auth-logto.test.js +233 -0
- package/dist/__tests__/auth-logto.test.js.map +1 -0
- package/dist/auth-logto.d.ts +85 -0
- package/dist/auth-logto.d.ts.map +1 -0
- package/dist/auth-logto.js +159 -0
- package/dist/auth-logto.js.map +1 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Gefter / 200apps Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @nwire/auth-logto
|
|
2
|
+
|
|
3
|
+
> IdP adapter for [Logto](https://logto.io) — hosted OIDC provider with JWKS verification.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
Verifies Logto-issued JWTs against JWKS (issuer + audience checks), maps claims to the `User` type (id, email, name, roles, scopes), and handles refresh + sign-out via Logto's OIDC endpoints. `signIn` intentionally throws — Logto owns the password/social/MFA flows via its hosted UI; apps redirect users there.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add @nwire/auth-logto @nwire/auth jose
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { logtoAdapter } from "@nwire/auth-logto";
|
|
19
|
+
import { identityPlugin } from "@nwire/auth";
|
|
20
|
+
import { defineApp } from "@nwire/forge";
|
|
21
|
+
|
|
22
|
+
defineApp("my-app", {
|
|
23
|
+
plugins: [
|
|
24
|
+
identityPlugin({
|
|
25
|
+
adapter: logtoAdapter({
|
|
26
|
+
endpoint: process.env.LOGTO_ENDPOINT!, // https://my-org.logto.app
|
|
27
|
+
audience: process.env.LOGTO_AUDIENCE!, // https://api.my-app.com
|
|
28
|
+
clientId: process.env.LOGTO_CLIENT_ID,
|
|
29
|
+
clientSecret: process.env.LOGTO_SECRET,
|
|
30
|
+
fetchUserInfo: true,
|
|
31
|
+
}),
|
|
32
|
+
}),
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API surface
|
|
38
|
+
|
|
39
|
+
- `logtoAdapter({ endpoint, audience, clientId?, clientSecret?, fetchUserInfo? })` — produces an `IdpAdapter`.
|
|
40
|
+
|
|
41
|
+
## When to use
|
|
42
|
+
|
|
43
|
+
When you want a managed IdP and don't want to own user storage / MFA / passkeys. Fits L3 and up.
|
|
44
|
+
|
|
45
|
+
## Standalone use
|
|
46
|
+
|
|
47
|
+
For developers using `@nwire/auth-logto` **without the rest of Nwire** — pair it with any TypeScript project, any container, any HTTP framework.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// See the package's main entry (src/) for the standalone surface.
|
|
51
|
+
// The exports below work without @nwire/app or @nwire/forge.
|
|
52
|
+
import {} from /* ...standalone exports... */ "@nwire/auth-logto";
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Within nwire-app
|
|
56
|
+
|
|
57
|
+
For developers using this package as part of the Nwire stack — register it via `app.use(...)` or it auto-wires when you compose `createApp({ modules })`.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { createApp } from "@nwire/forge";
|
|
61
|
+
|
|
62
|
+
const app = createApp({
|
|
63
|
+
/* ...config... */
|
|
64
|
+
});
|
|
65
|
+
// Adapter/plugin wiring happens here when applicable.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## See also
|
|
69
|
+
|
|
70
|
+
- [Architecture sketch §05 — Adapters tier](../../architecture-sketch.html#packages)
|
|
71
|
+
- Sibling packages: [@nwire/auth](../nwire-auth), [@nwire/auth-better-auth](../nwire-auth-better-auth), [@nwire/rbac](../nwire-rbac)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logto adapter — unit tests against locally-minted JWTs.
|
|
3
|
+
*
|
|
4
|
+
* We don't need a real Logto running for these. We do need:
|
|
5
|
+
* - a private/public keypair we generate at test setup
|
|
6
|
+
* - a fake `fetch` that:
|
|
7
|
+
* a) serves the JWKS at GET <endpoint>/oidc/jwks
|
|
8
|
+
* b) serves the userinfo at GET <endpoint>/oidc/me
|
|
9
|
+
* c) handles the refresh + signOut POSTs
|
|
10
|
+
*
|
|
11
|
+
* That covers every path in the adapter without booting a container. The
|
|
12
|
+
* real-Logto e2e (gated behind LOGTO_E2E=1) runs against the docker-compose
|
|
13
|
+
* Logto in Phase 62.3.3.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=auth-logto.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-logto.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/auth-logto.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logto adapter — unit tests against locally-minted JWTs.
|
|
3
|
+
*
|
|
4
|
+
* We don't need a real Logto running for these. We do need:
|
|
5
|
+
* - a private/public keypair we generate at test setup
|
|
6
|
+
* - a fake `fetch` that:
|
|
7
|
+
* a) serves the JWKS at GET <endpoint>/oidc/jwks
|
|
8
|
+
* b) serves the userinfo at GET <endpoint>/oidc/me
|
|
9
|
+
* c) handles the refresh + signOut POSTs
|
|
10
|
+
*
|
|
11
|
+
* That covers every path in the adapter without booting a container. The
|
|
12
|
+
* real-Logto e2e (gated behind LOGTO_E2E=1) runs against the docker-compose
|
|
13
|
+
* Logto in Phase 62.3.3.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
16
|
+
import { exportJWK, generateKeyPair, SignJWT } from "jose";
|
|
17
|
+
import { logtoAdapter } from "../auth-logto.js";
|
|
18
|
+
const ENDPOINT = "https://test-logto.example.com";
|
|
19
|
+
const AUDIENCE = "https://api.test.example.com";
|
|
20
|
+
const ISSUER = `${ENDPOINT}/oidc`;
|
|
21
|
+
const KID = "test-kid-1";
|
|
22
|
+
// jose returns CryptoKey-like objects; we don't need the DOM lib for the
|
|
23
|
+
// test, so type them as `unknown` and let jose handle them internally.
|
|
24
|
+
let privateKey;
|
|
25
|
+
let publicJwk;
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
const kp = await generateKeyPair("ES256");
|
|
28
|
+
privateKey = kp.privateKey;
|
|
29
|
+
publicJwk = { ...(await exportJWK(kp.publicKey)), kid: KID, alg: "ES256", use: "sig" };
|
|
30
|
+
});
|
|
31
|
+
async function mintToken(input) {
|
|
32
|
+
return new SignJWT({
|
|
33
|
+
scope: input.scope,
|
|
34
|
+
roles: input.roles,
|
|
35
|
+
})
|
|
36
|
+
.setProtectedHeader({ alg: "ES256", kid: KID })
|
|
37
|
+
.setSubject(input.sub)
|
|
38
|
+
.setIssuer(input.issuer ?? ISSUER)
|
|
39
|
+
.setAudience(input.aud ?? AUDIENCE)
|
|
40
|
+
.setIssuedAt()
|
|
41
|
+
.setExpirationTime(input.expiresIn ?? "5m")
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
.sign(privateKey);
|
|
44
|
+
}
|
|
45
|
+
function fakeFetch(overrides = {}) {
|
|
46
|
+
return (async (input, init) => {
|
|
47
|
+
const url = typeof input === "string"
|
|
48
|
+
? input
|
|
49
|
+
: input instanceof URL
|
|
50
|
+
? input.toString()
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
: input?.url;
|
|
53
|
+
if (overrides[url])
|
|
54
|
+
return overrides[url]();
|
|
55
|
+
// Default: serve JWKS.
|
|
56
|
+
if (url.endsWith("/oidc/jwks")) {
|
|
57
|
+
return new Response(JSON.stringify({ keys: [publicJwk] }), {
|
|
58
|
+
status: 200,
|
|
59
|
+
headers: { "content-type": "application/json" },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Default: userinfo.
|
|
63
|
+
if (url.endsWith("/oidc/me")) {
|
|
64
|
+
const auth = init?.headers && init.headers["Authorization"];
|
|
65
|
+
if (!auth?.startsWith("Bearer ")) {
|
|
66
|
+
return new Response("Unauthorized", { status: 401 });
|
|
67
|
+
}
|
|
68
|
+
return new Response(JSON.stringify({
|
|
69
|
+
sub: "user_42",
|
|
70
|
+
email: "alex@example.com",
|
|
71
|
+
name: "Alex",
|
|
72
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
73
|
+
}
|
|
74
|
+
// Default: refresh
|
|
75
|
+
if (url.endsWith("/oidc/token")) {
|
|
76
|
+
return new Response(JSON.stringify({
|
|
77
|
+
access_token: "new-access-token",
|
|
78
|
+
refresh_token: "new-refresh-token",
|
|
79
|
+
expires_in: 3600,
|
|
80
|
+
token_type: "Bearer",
|
|
81
|
+
}), { status: 200, headers: { "content-type": "application/json" } });
|
|
82
|
+
}
|
|
83
|
+
// Default: session/end
|
|
84
|
+
if (url.endsWith("/oidc/session/end")) {
|
|
85
|
+
return new Response(null, { status: 204 });
|
|
86
|
+
}
|
|
87
|
+
return new Response("Not Found", { status: 404 });
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
describe("logtoAdapter — verifyToken", () => {
|
|
91
|
+
it("returns User for a valid token (no userinfo fetch)", async () => {
|
|
92
|
+
const adapter = logtoAdapter({
|
|
93
|
+
endpoint: ENDPOINT,
|
|
94
|
+
audience: AUDIENCE,
|
|
95
|
+
jwks: { keys: [publicJwk] },
|
|
96
|
+
fetch: fakeFetch(),
|
|
97
|
+
});
|
|
98
|
+
const token = await mintToken({
|
|
99
|
+
sub: "user_42",
|
|
100
|
+
scope: "read:posts write:posts",
|
|
101
|
+
roles: ["editor"],
|
|
102
|
+
});
|
|
103
|
+
const user = await adapter.verifyToken(token);
|
|
104
|
+
expect(user?.id).toBe("user_42");
|
|
105
|
+
expect(user?.scopes).toEqual(["read:posts", "write:posts"]);
|
|
106
|
+
expect(user?.roles).toEqual(["editor"]);
|
|
107
|
+
expect(user?.email).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
it("enriches with /oidc/me when fetchUserInfo is true", async () => {
|
|
110
|
+
const adapter = logtoAdapter({
|
|
111
|
+
endpoint: ENDPOINT,
|
|
112
|
+
audience: AUDIENCE,
|
|
113
|
+
fetchUserInfo: true,
|
|
114
|
+
jwks: { keys: [publicJwk] },
|
|
115
|
+
fetch: fakeFetch(),
|
|
116
|
+
});
|
|
117
|
+
const token = await mintToken({ sub: "user_42", scope: "openid" });
|
|
118
|
+
const user = await adapter.verifyToken(token);
|
|
119
|
+
expect(user?.email).toBe("alex@example.com");
|
|
120
|
+
expect(user?.name).toBe("Alex");
|
|
121
|
+
});
|
|
122
|
+
it("returns null for a wrong-audience token", async () => {
|
|
123
|
+
const adapter = logtoAdapter({
|
|
124
|
+
endpoint: ENDPOINT,
|
|
125
|
+
audience: AUDIENCE,
|
|
126
|
+
jwks: { keys: [publicJwk] },
|
|
127
|
+
fetch: fakeFetch(),
|
|
128
|
+
});
|
|
129
|
+
const token = await mintToken({ sub: "user_42", aud: "https://different.audience" });
|
|
130
|
+
expect(await adapter.verifyToken(token)).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
it("returns null for an expired token", async () => {
|
|
133
|
+
const adapter = logtoAdapter({
|
|
134
|
+
endpoint: ENDPOINT,
|
|
135
|
+
audience: AUDIENCE,
|
|
136
|
+
clockTolerance: 0,
|
|
137
|
+
jwks: { keys: [publicJwk] },
|
|
138
|
+
fetch: fakeFetch(),
|
|
139
|
+
});
|
|
140
|
+
const token = await mintToken({ sub: "user_42", expiresIn: "-1s" });
|
|
141
|
+
expect(await adapter.verifyToken(token)).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
it("returns null for a wrong-issuer token", async () => {
|
|
144
|
+
const adapter = logtoAdapter({
|
|
145
|
+
endpoint: ENDPOINT,
|
|
146
|
+
audience: AUDIENCE,
|
|
147
|
+
jwks: { keys: [publicJwk] },
|
|
148
|
+
fetch: fakeFetch(),
|
|
149
|
+
});
|
|
150
|
+
const token = await mintToken({
|
|
151
|
+
sub: "user_42",
|
|
152
|
+
issuer: "https://attacker.example.com/oidc",
|
|
153
|
+
});
|
|
154
|
+
expect(await adapter.verifyToken(token)).toBeNull();
|
|
155
|
+
});
|
|
156
|
+
it("returns null for a garbage token", async () => {
|
|
157
|
+
const adapter = logtoAdapter({
|
|
158
|
+
endpoint: ENDPOINT,
|
|
159
|
+
audience: AUDIENCE,
|
|
160
|
+
jwks: { keys: [publicJwk] },
|
|
161
|
+
fetch: fakeFetch(),
|
|
162
|
+
});
|
|
163
|
+
expect(await adapter.verifyToken("not.a.jwt")).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
it("falls back to base user when /oidc/me errors (non-fatal)", async () => {
|
|
166
|
+
const adapter = logtoAdapter({
|
|
167
|
+
endpoint: ENDPOINT,
|
|
168
|
+
audience: AUDIENCE,
|
|
169
|
+
fetchUserInfo: true,
|
|
170
|
+
jwks: { keys: [publicJwk] },
|
|
171
|
+
fetch: fakeFetch({
|
|
172
|
+
[`${ENDPOINT}/oidc/me`]: () => new Response("boom", { status: 500 }),
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
const token = await mintToken({ sub: "user_42", scope: "read:posts" });
|
|
176
|
+
const user = await adapter.verifyToken(token);
|
|
177
|
+
expect(user?.id).toBe("user_42");
|
|
178
|
+
expect(user?.scopes).toEqual(["read:posts"]);
|
|
179
|
+
expect(user?.email).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe("logtoAdapter — refresh", () => {
|
|
183
|
+
it("posts to /oidc/token + returns new tokens", async () => {
|
|
184
|
+
const adapter = logtoAdapter({
|
|
185
|
+
endpoint: ENDPOINT,
|
|
186
|
+
audience: AUDIENCE,
|
|
187
|
+
clientId: "test-client",
|
|
188
|
+
fetch: fakeFetch(),
|
|
189
|
+
});
|
|
190
|
+
const tokens = await adapter.refresh("old-refresh");
|
|
191
|
+
expect(tokens.accessToken).toBe("new-access-token");
|
|
192
|
+
expect(tokens.refreshToken).toBe("new-refresh-token");
|
|
193
|
+
expect(tokens.tokenType).toBe("Bearer");
|
|
194
|
+
});
|
|
195
|
+
it("throws when clientId is missing", async () => {
|
|
196
|
+
const adapter = logtoAdapter({
|
|
197
|
+
endpoint: ENDPOINT,
|
|
198
|
+
audience: AUDIENCE,
|
|
199
|
+
jwks: { keys: [publicJwk] },
|
|
200
|
+
fetch: fakeFetch(),
|
|
201
|
+
});
|
|
202
|
+
await expect(adapter.refresh("x")).rejects.toThrow(/clientId/);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe("logtoAdapter — signOut", () => {
|
|
206
|
+
it("posts to /oidc/session/end", async () => {
|
|
207
|
+
let called = false;
|
|
208
|
+
const adapter = logtoAdapter({
|
|
209
|
+
endpoint: ENDPOINT,
|
|
210
|
+
audience: AUDIENCE,
|
|
211
|
+
fetch: fakeFetch({
|
|
212
|
+
[`${ENDPOINT}/oidc/session/end`]: () => {
|
|
213
|
+
called = true;
|
|
214
|
+
return new Response(null, { status: 204 });
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
await adapter.signOut("some-id-token");
|
|
219
|
+
expect(called).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe("logtoAdapter — signIn intentionally rejects", () => {
|
|
223
|
+
it("throws because Logto uses OIDC redirect flow", async () => {
|
|
224
|
+
const adapter = logtoAdapter({
|
|
225
|
+
endpoint: ENDPOINT,
|
|
226
|
+
audience: AUDIENCE,
|
|
227
|
+
jwks: { keys: [publicJwk] },
|
|
228
|
+
fetch: fakeFetch(),
|
|
229
|
+
});
|
|
230
|
+
await expect(adapter.signIn({ email: "a@x", password: "x" })).rejects.toThrow(/redirect flow/i);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
//# sourceMappingURL=auth-logto.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-logto.test.js","sourceRoot":"","sources":["../../src/__tests__/auth-logto.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/auth-logto` — IdP adapter for Logto.
|
|
3
|
+
*
|
|
4
|
+
* import { logtoAdapter } from "@nwire/auth-logto";
|
|
5
|
+
* import { identityPlugin } from "@nwire/auth";
|
|
6
|
+
*
|
|
7
|
+
* defineApp("my-app", {
|
|
8
|
+
* plugins: [
|
|
9
|
+
* identityPlugin({
|
|
10
|
+
* adapter: logtoAdapter({
|
|
11
|
+
* endpoint: process.env.LOGTO_ENDPOINT!, // https://my-org.logto.app
|
|
12
|
+
* audience: process.env.LOGTO_AUDIENCE!, // https://api.my-app.com
|
|
13
|
+
* clientId: process.env.LOGTO_CLIENT_ID, // for refresh + logout
|
|
14
|
+
* clientSecret: process.env.LOGTO_SECRET,
|
|
15
|
+
* fetchUserInfo: true, // call /oidc/me to enrich
|
|
16
|
+
* }),
|
|
17
|
+
* }),
|
|
18
|
+
* ],
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* What it does:
|
|
22
|
+
* - `verifyToken(jwt)` — verifies signature against Logto's JWKS,
|
|
23
|
+
* checks issuer + audience, maps claims to User (id, email, name,
|
|
24
|
+
* roles, scopes).
|
|
25
|
+
* - `signOut(token)` — POSTs to Logto's session-end endpoint.
|
|
26
|
+
* - `refresh(refreshToken)` — POSTs to Logto's `/oidc/token` with the
|
|
27
|
+
* `refresh_token` grant; returns the new access token bundle.
|
|
28
|
+
* - `signIn` intentionally throws — Logto is an OIDC IdP that owns
|
|
29
|
+
* password / social / MFA flows via its hosted UI. Apps redirect
|
|
30
|
+
* users there, not through this adapter. (The OIDC redirect-flow
|
|
31
|
+
* resolvers — startSignIn / completeSignIn — are a future addition.)
|
|
32
|
+
*
|
|
33
|
+
* Why glue: we do JWT verification + userinfo with `jose`, the standard
|
|
34
|
+
* Node JWT library. No big SDK. The adapter is ~150 lines because Logto
|
|
35
|
+
* itself owns 99% of the complexity (user database, password hashing,
|
|
36
|
+
* social providers, MFA, role management, audit log, admin console).
|
|
37
|
+
*/
|
|
38
|
+
import { type JSONWebKeySet } from "jose";
|
|
39
|
+
import type { IdpAdapter } from "@nwire/auth";
|
|
40
|
+
export interface LogtoAdapterOptions {
|
|
41
|
+
/** Logto endpoint — `https://your-tenant.logto.app` or your self-hosted URL. */
|
|
42
|
+
readonly endpoint: string;
|
|
43
|
+
/**
|
|
44
|
+
* API resource indicator — the same string you used when registering
|
|
45
|
+
* the API resource in Logto's admin console. Logto issues tokens with
|
|
46
|
+
* `aud: audience`; we enforce that on verification. Omit only if you
|
|
47
|
+
* accept tokens issued for the management API.
|
|
48
|
+
*/
|
|
49
|
+
readonly audience?: string;
|
|
50
|
+
/**
|
|
51
|
+
* Application client id — required for the `refresh_token` grant + the
|
|
52
|
+
* `end_session` endpoint. Get it from Logto's admin console.
|
|
53
|
+
*/
|
|
54
|
+
readonly clientId?: string;
|
|
55
|
+
/** Application client secret — needed by confidential clients. */
|
|
56
|
+
readonly clientSecret?: string;
|
|
57
|
+
/**
|
|
58
|
+
* If true, the adapter calls Logto's `/oidc/me` endpoint after verifying
|
|
59
|
+
* a token and merges the userinfo into the User (email, name, picture,
|
|
60
|
+
* custom fields). Costs one extra HTTP request per `verifyToken` call;
|
|
61
|
+
* disable if your JWT claims already carry everything you need.
|
|
62
|
+
*/
|
|
63
|
+
readonly fetchUserInfo?: boolean;
|
|
64
|
+
/** Optional clock skew tolerance for `exp`/`nbf` in seconds. Default 30. */
|
|
65
|
+
readonly clockTolerance?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Inject a fetch impl for tests + the `signOut` / `refresh` / userinfo
|
|
68
|
+
* calls. The remote JWKS fetch uses this when supplied, but jose's
|
|
69
|
+
* `createRemoteJWKSet` doesn't accept a fetch override — so for the
|
|
70
|
+
* JWKS path, prefer the `jwks` option below.
|
|
71
|
+
*/
|
|
72
|
+
readonly fetch?: typeof fetch;
|
|
73
|
+
/**
|
|
74
|
+
* Pass a pre-fetched `JSONWebKeySet` to skip the remote fetch entirely.
|
|
75
|
+
* Used in tests + in production setups that mirror Logto's JWKS into
|
|
76
|
+
* memory/Redis on a refresh schedule.
|
|
77
|
+
*/
|
|
78
|
+
readonly jwks?: JSONWebKeySet;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build a Logto IdP adapter. Constructs the JWKS reference lazily (no
|
|
82
|
+
* HTTP call at boot — the first `verifyToken` lazy-loads keys).
|
|
83
|
+
*/
|
|
84
|
+
export declare function logtoAdapter(options: LogtoAdapterOptions): IdpAdapter;
|
|
85
|
+
//# sourceMappingURL=auth-logto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-logto.d.ts","sourceRoot":"","sources":["../src/auth-logto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,EAIL,KAAK,aAAa,EAGnB,MAAM,MAAM,CAAC;AACd,OAAO,KAAK,EAAc,UAAU,EAAqB,MAAM,aAAa,CAAC;AAE7E,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,aAAa,CAAC;CAC/B;AAaD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,UAAU,CAiIrE"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nwire/auth-logto` — IdP adapter for Logto.
|
|
3
|
+
*
|
|
4
|
+
* import { logtoAdapter } from "@nwire/auth-logto";
|
|
5
|
+
* import { identityPlugin } from "@nwire/auth";
|
|
6
|
+
*
|
|
7
|
+
* defineApp("my-app", {
|
|
8
|
+
* plugins: [
|
|
9
|
+
* identityPlugin({
|
|
10
|
+
* adapter: logtoAdapter({
|
|
11
|
+
* endpoint: process.env.LOGTO_ENDPOINT!, // https://my-org.logto.app
|
|
12
|
+
* audience: process.env.LOGTO_AUDIENCE!, // https://api.my-app.com
|
|
13
|
+
* clientId: process.env.LOGTO_CLIENT_ID, // for refresh + logout
|
|
14
|
+
* clientSecret: process.env.LOGTO_SECRET,
|
|
15
|
+
* fetchUserInfo: true, // call /oidc/me to enrich
|
|
16
|
+
* }),
|
|
17
|
+
* }),
|
|
18
|
+
* ],
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* What it does:
|
|
22
|
+
* - `verifyToken(jwt)` — verifies signature against Logto's JWKS,
|
|
23
|
+
* checks issuer + audience, maps claims to User (id, email, name,
|
|
24
|
+
* roles, scopes).
|
|
25
|
+
* - `signOut(token)` — POSTs to Logto's session-end endpoint.
|
|
26
|
+
* - `refresh(refreshToken)` — POSTs to Logto's `/oidc/token` with the
|
|
27
|
+
* `refresh_token` grant; returns the new access token bundle.
|
|
28
|
+
* - `signIn` intentionally throws — Logto is an OIDC IdP that owns
|
|
29
|
+
* password / social / MFA flows via its hosted UI. Apps redirect
|
|
30
|
+
* users there, not through this adapter. (The OIDC redirect-flow
|
|
31
|
+
* resolvers — startSignIn / completeSignIn — are a future addition.)
|
|
32
|
+
*
|
|
33
|
+
* Why glue: we do JWT verification + userinfo with `jose`, the standard
|
|
34
|
+
* Node JWT library. No big SDK. The adapter is ~150 lines because Logto
|
|
35
|
+
* itself owns 99% of the complexity (user database, password hashing,
|
|
36
|
+
* social providers, MFA, role management, audit log, admin console).
|
|
37
|
+
*/
|
|
38
|
+
import { createLocalJWKSet, createRemoteJWKSet, jwtVerify, } from "jose";
|
|
39
|
+
/**
|
|
40
|
+
* Build a Logto IdP adapter. Constructs the JWKS reference lazily (no
|
|
41
|
+
* HTTP call at boot — the first `verifyToken` lazy-loads keys).
|
|
42
|
+
*/
|
|
43
|
+
export function logtoAdapter(options) {
|
|
44
|
+
const endpoint = options.endpoint.replace(/\/$/, "");
|
|
45
|
+
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
46
|
+
const issuer = `${endpoint}/oidc`;
|
|
47
|
+
const jwks = options.jwks
|
|
48
|
+
? createLocalJWKSet(options.jwks)
|
|
49
|
+
: createRemoteJWKSet(new URL(`${endpoint}/oidc/jwks`));
|
|
50
|
+
const verifyOpts = {
|
|
51
|
+
issuer,
|
|
52
|
+
audience: options.audience,
|
|
53
|
+
clockTolerance: options.clockTolerance ?? 30,
|
|
54
|
+
};
|
|
55
|
+
async function payloadToUser(payload) {
|
|
56
|
+
const sub = String(payload.sub ?? "");
|
|
57
|
+
if (!sub)
|
|
58
|
+
throw new Error("Logto JWT has no `sub` claim");
|
|
59
|
+
const scopes = typeof payload.scope === "string"
|
|
60
|
+
? payload.scope.split(/\s+/).filter(Boolean)
|
|
61
|
+
: Array.isArray(payload.scope) ? payload.scope : [];
|
|
62
|
+
const claimRoles = payload.roles;
|
|
63
|
+
const roles = Array.isArray(claimRoles)
|
|
64
|
+
? claimRoles
|
|
65
|
+
: [];
|
|
66
|
+
let user = {
|
|
67
|
+
id: sub,
|
|
68
|
+
roles,
|
|
69
|
+
scopes,
|
|
70
|
+
};
|
|
71
|
+
if (options.fetchUserInfo && payload.iss) {
|
|
72
|
+
// /oidc/me must be called with the same access token in Authorization.
|
|
73
|
+
// We don't have the raw JWT here — verifyToken passes it via closure.
|
|
74
|
+
// See verifyToken below for the wiring.
|
|
75
|
+
}
|
|
76
|
+
return user;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
async verifyToken(token) {
|
|
80
|
+
try {
|
|
81
|
+
const { payload } = await jwtVerify(token, jwks, verifyOpts);
|
|
82
|
+
const base = await payloadToUser(payload);
|
|
83
|
+
if (!options.fetchUserInfo)
|
|
84
|
+
return base;
|
|
85
|
+
// Enrich with userinfo. Failures are logged but non-fatal — the
|
|
86
|
+
// token verified successfully, so the user identity is established
|
|
87
|
+
// even if the userinfo fetch hiccups.
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetchImpl(`${endpoint}/oidc/me`, {
|
|
90
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok)
|
|
93
|
+
return base;
|
|
94
|
+
const info = (await res.json());
|
|
95
|
+
return {
|
|
96
|
+
...base,
|
|
97
|
+
email: info.email,
|
|
98
|
+
name: info.name ?? info.username,
|
|
99
|
+
roles: info.roles ?? base.roles,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return base;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Bad signature, expired, wrong audience, malformed — all "no user".
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
async signIn(_input) {
|
|
112
|
+
throw new Error("@nwire/auth-logto: signIn() is not supported — Logto uses the OIDC " +
|
|
113
|
+
"redirect flow (hosted sign-in UI). Mount a redirect endpoint that " +
|
|
114
|
+
"sends users to Logto's authorize URL; verify the access token they " +
|
|
115
|
+
"return with via verifyToken().");
|
|
116
|
+
},
|
|
117
|
+
async signOut(token) {
|
|
118
|
+
// Logto's RFC 8414 logout endpoint accepts an `id_token_hint` to end
|
|
119
|
+
// the session. If callers pass a refresh token instead, we revoke it.
|
|
120
|
+
const res = await fetchImpl(`${endpoint}/oidc/session/end`, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
123
|
+
body: new URLSearchParams({ id_token_hint: token }).toString(),
|
|
124
|
+
});
|
|
125
|
+
if (!res.ok && res.status !== 302) {
|
|
126
|
+
throw new Error(`Logto signOut failed: ${res.status} ${res.statusText}`);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
async refresh(refreshToken) {
|
|
130
|
+
if (!options.clientId) {
|
|
131
|
+
throw new Error("@nwire/auth-logto: refresh() requires `clientId`");
|
|
132
|
+
}
|
|
133
|
+
const body = new URLSearchParams({
|
|
134
|
+
grant_type: "refresh_token",
|
|
135
|
+
refresh_token: refreshToken,
|
|
136
|
+
client_id: options.clientId,
|
|
137
|
+
});
|
|
138
|
+
if (options.clientSecret)
|
|
139
|
+
body.set("client_secret", options.clientSecret);
|
|
140
|
+
const res = await fetchImpl(`${endpoint}/oidc/token`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
143
|
+
body: body.toString(),
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
throw new Error(`Logto refresh failed: ${res.status} ${res.statusText}`);
|
|
147
|
+
}
|
|
148
|
+
const data = (await res.json());
|
|
149
|
+
return {
|
|
150
|
+
accessToken: data.access_token,
|
|
151
|
+
refreshToken: data.refresh_token,
|
|
152
|
+
idToken: data.id_token,
|
|
153
|
+
expiresAt: data.expires_in ? Date.now() + data.expires_in * 1000 : undefined,
|
|
154
|
+
tokenType: "Bearer",
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=auth-logto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-logto.js","sourceRoot":"","sources":["../src/auth-logto.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nwire/auth-logto",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Nwire — Logto auth adapter. Verifies Logto-issued JWTs via JWKS; maps Logto claims (sub, email, roles, scope) to the canonical User; runs sign-out + refresh via Logto's OIDC endpoints. Sign-in is the hosted UI redirect flow; this adapter does not implement password grant.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"adapter",
|
|
7
|
+
"auth",
|
|
8
|
+
"idp",
|
|
9
|
+
"jwt",
|
|
10
|
+
"logto",
|
|
11
|
+
"nwire",
|
|
12
|
+
"oidc"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/auth-logto.js",
|
|
20
|
+
"types": "./dist/auth-logto.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/auth-logto.js",
|
|
24
|
+
"types": "./dist/auth-logto.d.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"jose": "^6.2.3",
|
|
32
|
+
"@nwire/auth": "0.7.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.19.9",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vitest": "^4.1.6"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
|
|
41
|
+
"dev": "tsc --watch",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
}
|
|
44
|
+
}
|