@baseworks/auth 0.2.0 → 0.2.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/README.md +233 -0
- package/dist/{chunk-M7EACPIB.js → chunk-34E5PYNA.js} +4 -9
- package/dist/{chunk-VUB4GTMI.js → chunk-NBKMWUCN.js} +13 -27
- package/dist/chunk-TXDBQMTY.js +23 -0
- package/dist/edge.js +16 -15
- package/dist/hono.d.ts +26 -0
- package/dist/hono.js +24 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +8 -8
- package/dist/jwt.d.ts +17 -0
- package/dist/jwt.js +6 -0
- package/dist/pkce.d.ts +0 -4
- package/dist/pkce.js +1 -1
- package/dist/session.js +2 -1
- package/package.json +9 -3
- package/src/_internal.ts +14 -0
- package/src/edge.ts +1 -16
- package/src/hono.ts +40 -0
- package/src/index.ts +4 -3
- package/src/jwt.ts +39 -0
- package/src/pkce.ts +10 -23
- package/src/session.ts +11 -34
- package/dist/chunk-BMPRMOI7.js +0 -23
- package/dist/zitadel.d.ts +0 -28
- package/dist/zitadel.js +0 -6
- package/src/zitadel.ts +0 -56
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# @baseworks/auth
|
|
2
|
+
|
|
3
|
+
Authentication utilities for the baseworks monorepo. Uses subpath imports so each environment only pulls in what it needs.
|
|
4
|
+
|
|
5
|
+
## Subpath Imports
|
|
6
|
+
|
|
7
|
+
| Import | Runtime | Contents |
|
|
8
|
+
|--------|---------|----------|
|
|
9
|
+
| `@baseworks/auth` | Any | token helpers, verifyHs256Jwt, verifyOidcToken, pkce, cli-auth, url-helpers |
|
|
10
|
+
| `@baseworks/auth/jwt` | Node.js | HS256 JWT verification |
|
|
11
|
+
| `@baseworks/auth/hono` | Node.js + Hono | `requireAuth` middleware |
|
|
12
|
+
| `@baseworks/auth/oidc` | Workers / Node / Browser | OIDC token verification via JWKS |
|
|
13
|
+
| `@baseworks/auth/oidc-human` | Any | OIDC → DB user resolver factory |
|
|
14
|
+
| `@baseworks/auth/pkce` | Workers / Node / Browser | PKCE generation + OIDC auth URL builder |
|
|
15
|
+
| `@baseworks/auth/token` | Workers / Node / Browser | hashToken, looksLikeJwt, stripBearer |
|
|
16
|
+
| `@baseworks/auth/cli-auth` | Node.js CLI | Browser-based polling auth flow |
|
|
17
|
+
| `@baseworks/auth/session` | Next.js (Server) | OIDC cookie session, token exchange, refresh |
|
|
18
|
+
| `@baseworks/auth/edge` | Next.js (Edge/Server) | Pomerium JWT assertions + cookie fallback |
|
|
19
|
+
| `@baseworks/auth/url-helpers` | Next.js | Auth/account public URL builders |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## File Reference
|
|
24
|
+
|
|
25
|
+
### `jwt.ts` → `@baseworks/auth/jwt`
|
|
26
|
+
**Runtime:** Node.js — uses `node:crypto`
|
|
27
|
+
|
|
28
|
+
HS256 JWT verification. Used by Flect services (`org-service`, `account-service`) to verify service-to-service JWTs signed with `JWT_SECRET`.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { verifyHs256Jwt } from '@baseworks/auth/jwt'
|
|
32
|
+
|
|
33
|
+
const claims = verifyHs256Jwt(token, process.env.JWT_SECRET)
|
|
34
|
+
// null → invalid signature or expired
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
### `hono.ts` → `@baseworks/auth/hono`
|
|
40
|
+
**Runtime:** Node.js + Hono — peer dep: `hono`
|
|
41
|
+
|
|
42
|
+
Hono middleware. Reads `Authorization: Bearer <token>`, verifies with `jwt.ts`, sets `c.var.auth` (typed as `ServiceAuthUser`).
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { requireAuth } from '@baseworks/auth/hono'
|
|
46
|
+
|
|
47
|
+
app.use('*', requireAuth)
|
|
48
|
+
app.get('/me', (c) => {
|
|
49
|
+
const { userId, orgId } = c.var.auth
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> `ContextVariableMap` is augmented here — do not re-declare it in services.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### `oidc.ts` → `@baseworks/auth/oidc`
|
|
58
|
+
**Runtime:** Workers / Node / Browser — dep: `jose`
|
|
59
|
+
|
|
60
|
+
Provider-agnostic OIDC JWT verification. Fetches the public key from the JWKS endpoint, verifies the signature, and returns a normalised `OidcIdentity`.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { verifyOidcToken } from '@baseworks/auth/oidc'
|
|
64
|
+
|
|
65
|
+
const identity = await verifyOidcToken(bearerToken, {
|
|
66
|
+
issuer: 'https://auth.example.com',
|
|
67
|
+
audience: 'my-app',
|
|
68
|
+
})
|
|
69
|
+
// null → verification failed
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
JWKS endpoint: `{issuer}/oauth/v2/keys` (Zitadel-compatible).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
### `oidc-human.ts` → `@baseworks/auth/oidc-human`
|
|
77
|
+
**Runtime:** Any — dep: `oidc.ts`, `jose`
|
|
78
|
+
|
|
79
|
+
Combines `verifyOidcToken` with a DB user upsert into a single resolver. The `findOrCreate` callback stays app-specific.
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { createOidcHumanResolver } from '@baseworks/auth/oidc-human'
|
|
83
|
+
|
|
84
|
+
const resolve = createOidcHumanResolver({
|
|
85
|
+
issuer: process.env.OIDC_ISSUER,
|
|
86
|
+
findOrCreate: async (identity) => db.users.upsert(identity),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const user = await resolve(bearerToken) // null → verification failed
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Fast-fail: if the issuer doesn't match it returns null before hitting the JWKS endpoint.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### `pkce.ts` → `@baseworks/auth/pkce`
|
|
97
|
+
**Runtime:** Workers / Node / Browser — dep: Web Crypto API, `@baseworks/core`
|
|
98
|
+
|
|
99
|
+
PKCE (RFC 7636) `code_verifier` + `code_challenge` generation. OIDC authorization URL builder compatible with any provider (Zitadel, Auth0, Keycloak, etc.).
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { generatePkce, buildOidcAuthUrl } from '@baseworks/auth/pkce'
|
|
103
|
+
|
|
104
|
+
const { verifier, challenge } = await generatePkce()
|
|
105
|
+
const url = buildOidcAuthUrl({ issuer, clientId, redirectUri, challenge })
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### `token.ts` → `@baseworks/auth/token`
|
|
111
|
+
**Runtime:** Workers / Node / Browser — dep: Web Crypto API
|
|
112
|
+
|
|
113
|
+
General-purpose token helpers.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { hashToken, looksLikeJwt, stripBearer } from '@baseworks/auth/token'
|
|
117
|
+
|
|
118
|
+
const hash = await hashToken(rawToken, 'optional-pepper') // SHA-256 hex
|
|
119
|
+
const raw = stripBearer(request.headers.get('Authorization')) // null if missing
|
|
120
|
+
const isJwt = looksLikeJwt(token) // true if 3-part dot-separated
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### `cli-auth.ts` → `@baseworks/auth/cli-auth`
|
|
126
|
+
**Runtime:** Node.js CLI — dep: `fetch`
|
|
127
|
+
|
|
128
|
+
CLI → browser auth polling flow. Used by commands like `flect login`. The user opens a URL in the browser; the CLI polls until auth completes.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { startCliAuth, pollCliAuth } from '@baseworks/auth/cli-auth'
|
|
132
|
+
|
|
133
|
+
const { state, url } = await startCliAuth('https://api.flect.run')
|
|
134
|
+
console.log(`Open: ${url}`)
|
|
135
|
+
const { token } = await pollCliAuth('https://api.flect.run', state)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### `session.ts` → `@baseworks/auth/session`
|
|
141
|
+
**Runtime:** Next.js (Server Components, Route Handlers) — dep: `next`
|
|
142
|
+
|
|
143
|
+
Full OIDC PKCE session management for Next.js. Handles cookie set/clear, authorization code exchange, and token refresh. Uses `@baseworks/core` for base64url encoding and `pkce.ts` for PKCE generation.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import {
|
|
147
|
+
buildAuthorizationRedirect,
|
|
148
|
+
handleAuthorizationCallback,
|
|
149
|
+
getSessionFromCookies,
|
|
150
|
+
getServerAccessToken,
|
|
151
|
+
buildLogoutResponse,
|
|
152
|
+
} from '@baseworks/auth/session'
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Reads env vars: `OIDC_ISSUER`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` (optional).
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### `edge.ts` → `@baseworks/auth/edge`
|
|
160
|
+
**Runtime:** Next.js (Middleware, Server Components) — dep: `next`
|
|
161
|
+
|
|
162
|
+
Reads the session from two sources in priority order: **Pomerium JWT assertion headers** (`x-pomerium-jwt-assertion`) first, then **OIDC cookie** fallback. Returns an `EdgeSession`.
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { getEdgeSession } from '@baseworks/auth/edge'
|
|
166
|
+
|
|
167
|
+
const session = await getEdgeSession()
|
|
168
|
+
// session.provider: 'pomerium' | 'cookie' | 'anonymous'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### `url-helpers.ts` → `@baseworks/auth/url-helpers`
|
|
174
|
+
**Runtime:** Next.js — dep: none
|
|
175
|
+
|
|
176
|
+
Reads Next.js env vars and exposes URL builder functions.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { buildAuthLoginUrl, buildAuthLogoutUrl } from '@baseworks/auth/url-helpers'
|
|
180
|
+
|
|
181
|
+
const loginUrl = buildAuthLoginUrl('/dashboard')
|
|
182
|
+
const logoutUrl = buildAuthLogoutUrl('/')
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Env vars read: `NEXT_PUBLIC_AUTH_URL`, `NEXT_PUBLIC_ACCOUNT_URL`, `APP_PUBLIC_URL`, `NEXT_PUBLIC_SITE_URL`.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### `_internal.ts` — not exported
|
|
190
|
+
Shared internal helpers. Not part of the public API.
|
|
191
|
+
|
|
192
|
+
- `parseJwtPayload(token)` — splits a JWT and decodes the payload using `@baseworks/core/codec`. Used by `session.ts` and `edge.ts`.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Internal Dependencies
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
@baseworks/core/codec
|
|
200
|
+
└── base64urlEncode / base64urlDecodeString
|
|
201
|
+
├── pkce.ts (PKCE verifier/challenge generation)
|
|
202
|
+
├── session.ts (cookie encode/decode)
|
|
203
|
+
└── _internal.ts (parseJwtPayload)
|
|
204
|
+
└── edge.ts
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## When to Use What
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
Service-to-service JWT verification (Hono/Node)
|
|
211
|
+
└─ @baseworks/auth/hono → requireAuth middleware
|
|
212
|
+
|
|
213
|
+
Raw HS256 JWT verify
|
|
214
|
+
└─ @baseworks/auth/jwt → verifyHs256Jwt
|
|
215
|
+
|
|
216
|
+
User OIDC token verification (Workers/Node)
|
|
217
|
+
└─ @baseworks/auth/oidc → verifyOidcToken
|
|
218
|
+
|
|
219
|
+
OIDC token + DB user sync in one step
|
|
220
|
+
└─ @baseworks/auth/oidc-human → createOidcHumanResolver
|
|
221
|
+
|
|
222
|
+
API key hashing / Bearer header parsing
|
|
223
|
+
└─ @baseworks/auth/token → hashToken / stripBearer
|
|
224
|
+
|
|
225
|
+
Next.js OIDC login / callback / logout
|
|
226
|
+
└─ @baseworks/auth/session → buildAuthorizationRedirect / handleAuthorizationCallback
|
|
227
|
+
|
|
228
|
+
Who is this user in Next.js Middleware?
|
|
229
|
+
└─ @baseworks/auth/edge → getEdgeSession
|
|
230
|
+
|
|
231
|
+
CLI browser auth
|
|
232
|
+
└─ @baseworks/auth/cli-auth → startCliAuth / pollCliAuth
|
|
233
|
+
```
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
// src/pkce.ts
|
|
2
|
+
import { base64urlEncode } from "@baseworks/core";
|
|
2
3
|
async function generatePkce() {
|
|
3
|
-
const
|
|
4
|
-
crypto.
|
|
5
|
-
const
|
|
6
|
-
const encoded = new TextEncoder().encode(verifier);
|
|
7
|
-
const digest = await crypto.subtle.digest("SHA-256", encoded);
|
|
8
|
-
const challenge = base64url(new Uint8Array(digest));
|
|
4
|
+
const verifier = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
5
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
6
|
+
const challenge = base64urlEncode(new Uint8Array(digest));
|
|
9
7
|
return { verifier, challenge, method: "S256" };
|
|
10
8
|
}
|
|
11
9
|
function buildOidcAuthUrl(config) {
|
|
@@ -23,9 +21,6 @@ function buildOidcAuthUrl(config) {
|
|
|
23
21
|
if (config.prompt) params.set("prompt", config.prompt);
|
|
24
22
|
return `${issuer}/oauth/v2/authorize?${params.toString()}`;
|
|
25
23
|
}
|
|
26
|
-
function base64url(buf) {
|
|
27
|
-
return btoa(Array.from(buf, (b) => String.fromCharCode(b)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
28
|
-
}
|
|
29
24
|
|
|
30
25
|
export {
|
|
31
26
|
generatePkce,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
generatePkce
|
|
3
|
+
} from "./chunk-34E5PYNA.js";
|
|
1
4
|
import {
|
|
2
5
|
getAuthPublicUrl,
|
|
3
6
|
normalizeUrlLike
|
|
@@ -6,6 +9,7 @@ import {
|
|
|
6
9
|
// src/session.ts
|
|
7
10
|
import { cookies } from "next/headers";
|
|
8
11
|
import { NextResponse } from "next/server";
|
|
12
|
+
import { base64urlEncode, base64urlDecodeString } from "@baseworks/core";
|
|
9
13
|
var oidcCookies = {
|
|
10
14
|
codeVerifier: "oidc_code_verifier",
|
|
11
15
|
state: "oidc_state",
|
|
@@ -56,26 +60,8 @@ function resolvePublicRedirect(target, request) {
|
|
|
56
60
|
if (publicSiteUrl) return new URL(target, publicSiteUrl);
|
|
57
61
|
return new URL(target, request.url);
|
|
58
62
|
}
|
|
59
|
-
function base64UrlEncode(input) {
|
|
60
|
-
const buffer = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input instanceof Uint8Array ? input : new Uint8Array(input));
|
|
61
|
-
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
62
|
-
}
|
|
63
|
-
function base64UrlDecode(input) {
|
|
64
|
-
const padded = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
65
|
-
const remainder = padded.length % 4;
|
|
66
|
-
const normalized = remainder === 0 ? padded : `${padded}${"=".repeat(4 - remainder)}`;
|
|
67
|
-
return Buffer.from(normalized, "base64").toString("utf8");
|
|
68
|
-
}
|
|
69
|
-
function sha256(input) {
|
|
70
|
-
return crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
71
|
-
}
|
|
72
63
|
function randomString() {
|
|
73
|
-
return
|
|
74
|
-
}
|
|
75
|
-
async function createPkcePair() {
|
|
76
|
-
const verifier = randomString();
|
|
77
|
-
const challenge = base64UrlEncode(await sha256(verifier));
|
|
78
|
-
return { challenge, verifier };
|
|
64
|
+
return base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
79
65
|
}
|
|
80
66
|
async function discoverOidc() {
|
|
81
67
|
const issuer = getIssuer();
|
|
@@ -84,26 +70,26 @@ async function discoverOidc() {
|
|
|
84
70
|
return await response.json();
|
|
85
71
|
}
|
|
86
72
|
function parseJwtPayload(token) {
|
|
87
|
-
const
|
|
88
|
-
return JSON.parse(
|
|
73
|
+
const part = token.split(".")[1] ?? "";
|
|
74
|
+
return JSON.parse(base64urlDecodeString(part));
|
|
89
75
|
}
|
|
90
76
|
function encodeSessionCookie(session) {
|
|
91
|
-
return
|
|
77
|
+
return base64urlEncode(new TextEncoder().encode(JSON.stringify(session)));
|
|
92
78
|
}
|
|
93
79
|
function parseSessionCookie(value) {
|
|
94
80
|
try {
|
|
95
|
-
const session = JSON.parse(
|
|
81
|
+
const session = JSON.parse(base64urlDecodeString(value));
|
|
96
82
|
return session.isAuthenticated ? session : null;
|
|
97
83
|
} catch {
|
|
98
84
|
return null;
|
|
99
85
|
}
|
|
100
86
|
}
|
|
101
87
|
function encodeTransactionCookie(transaction) {
|
|
102
|
-
return
|
|
88
|
+
return base64urlEncode(new TextEncoder().encode(JSON.stringify(transaction)));
|
|
103
89
|
}
|
|
104
90
|
function parseTransactionCookie(value) {
|
|
105
91
|
try {
|
|
106
|
-
const transaction = JSON.parse(
|
|
92
|
+
const transaction = JSON.parse(base64urlDecodeString(value));
|
|
107
93
|
if (!transaction.codeVerifier || !transaction.returnTo) return null;
|
|
108
94
|
return transaction;
|
|
109
95
|
} catch {
|
|
@@ -142,7 +128,7 @@ function getRedirectUri(request) {
|
|
|
142
128
|
}
|
|
143
129
|
function appendClientAuthentication(headers, params, clientId, clientSecret) {
|
|
144
130
|
if (clientSecret) {
|
|
145
|
-
const basic =
|
|
131
|
+
const basic = btoa(`${clientId}:${clientSecret}`);
|
|
146
132
|
headers.set("authorization", `Basic ${basic}`);
|
|
147
133
|
return;
|
|
148
134
|
}
|
|
@@ -235,7 +221,7 @@ function clearTransientCookies(response, request) {
|
|
|
235
221
|
async function buildAuthorizationRedirect(request) {
|
|
236
222
|
const discovery = await discoverOidc();
|
|
237
223
|
const clientId = getClientId();
|
|
238
|
-
const { challenge, verifier } = await
|
|
224
|
+
const { challenge, verifier } = await generatePkce();
|
|
239
225
|
const nonce = randomString();
|
|
240
226
|
const state = randomString();
|
|
241
227
|
const redirectUri = getRedirectUri(request);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/jwt.ts
|
|
2
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
3
|
+
function verifyHs256Jwt(token, secret) {
|
|
4
|
+
const parts = token.split(".");
|
|
5
|
+
if (parts.length !== 3) return null;
|
|
6
|
+
const [header, payload, sig] = parts;
|
|
7
|
+
const expected = createHmac("sha256", secret).update(`${header}.${payload}`).digest("base64url");
|
|
8
|
+
const sigBuf = Buffer.from(sig, "base64url");
|
|
9
|
+
const expBuf = Buffer.from(expected, "base64url");
|
|
10
|
+
if (sigBuf.length !== expBuf.length) return null;
|
|
11
|
+
if (!timingSafeEqual(sigBuf, expBuf)) return null;
|
|
12
|
+
try {
|
|
13
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
14
|
+
if (claims.exp && claims.exp < Math.floor(Date.now() / 1e3)) return null;
|
|
15
|
+
return claims;
|
|
16
|
+
} catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
verifyHs256Jwt
|
|
23
|
+
};
|
package/dist/edge.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getSessionFromCookies
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-NBKMWUCN.js";
|
|
4
|
+
import "./chunk-34E5PYNA.js";
|
|
4
5
|
import {
|
|
5
6
|
buildPublicUrl,
|
|
6
7
|
normalizeUrlLike
|
|
@@ -9,6 +10,20 @@ import {
|
|
|
9
10
|
// src/edge.ts
|
|
10
11
|
import { headers } from "next/headers";
|
|
11
12
|
import { redirect } from "next/navigation";
|
|
13
|
+
|
|
14
|
+
// src/_internal.ts
|
|
15
|
+
import { base64urlDecodeString } from "@baseworks/core";
|
|
16
|
+
function parseJwtPayload(token) {
|
|
17
|
+
try {
|
|
18
|
+
const part = token.split(".")[1];
|
|
19
|
+
if (!part) return null;
|
|
20
|
+
return JSON.parse(base64urlDecodeString(part));
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/edge.ts
|
|
12
27
|
function firstHeader(headerStore, names) {
|
|
13
28
|
for (const name of names) {
|
|
14
29
|
const value = headerStore.get(name);
|
|
@@ -16,20 +31,6 @@ function firstHeader(headerStore, names) {
|
|
|
16
31
|
}
|
|
17
32
|
return void 0;
|
|
18
33
|
}
|
|
19
|
-
function decodeBase64Url(value) {
|
|
20
|
-
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
21
|
-
const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
|
|
22
|
-
return Buffer.from(padded, "base64").toString("utf8");
|
|
23
|
-
}
|
|
24
|
-
function parseJwtPayload(token) {
|
|
25
|
-
try {
|
|
26
|
-
const [, payload] = token.split(".");
|
|
27
|
-
if (!payload) return null;
|
|
28
|
-
return JSON.parse(decodeBase64Url(payload));
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
34
|
function stringClaim(claims, keys) {
|
|
34
35
|
for (const key of keys) {
|
|
35
36
|
const value = claims[key];
|
package/dist/hono.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as hono from 'hono';
|
|
2
|
+
import { Context, Next } from 'hono';
|
|
3
|
+
|
|
4
|
+
interface ServiceAuthUser {
|
|
5
|
+
userId: string;
|
|
6
|
+
orgId?: string;
|
|
7
|
+
role?: string;
|
|
8
|
+
tokenType: 'user' | 'service';
|
|
9
|
+
}
|
|
10
|
+
declare module 'hono' {
|
|
11
|
+
interface ContextVariableMap {
|
|
12
|
+
auth: ServiceAuthUser;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Hono middleware — verifies HS256 JWT from Authorization: Bearer header.
|
|
17
|
+
* Reads JWT_SECRET from process.env.
|
|
18
|
+
* Sets c.var.auth on success.
|
|
19
|
+
*/
|
|
20
|
+
declare function requireAuth(c: Context, next: Next): Promise<void | (Response & hono.TypedResponse<{
|
|
21
|
+
error: string;
|
|
22
|
+
}, 401, "json">) | (Response & hono.TypedResponse<{
|
|
23
|
+
error: string;
|
|
24
|
+
}, 500, "json">)>;
|
|
25
|
+
|
|
26
|
+
export { type ServiceAuthUser, requireAuth };
|
package/dist/hono.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
verifyHs256Jwt
|
|
3
|
+
} from "./chunk-TXDBQMTY.js";
|
|
4
|
+
|
|
5
|
+
// src/hono.ts
|
|
6
|
+
async function requireAuth(c, next) {
|
|
7
|
+
const header = c.req.header("Authorization");
|
|
8
|
+
if (!header?.startsWith("Bearer ")) return c.json({ error: "Unauthorized" }, 401);
|
|
9
|
+
const token = header.slice(7);
|
|
10
|
+
const secret = process.env["JWT_SECRET"];
|
|
11
|
+
if (!secret) return c.json({ error: "JWT_SECRET not configured" }, 500);
|
|
12
|
+
const claims = verifyHs256Jwt(token, secret);
|
|
13
|
+
if (!claims) return c.json({ error: "Unauthorized" }, 401);
|
|
14
|
+
c.set("auth", {
|
|
15
|
+
userId: String(claims["sub"] ?? ""),
|
|
16
|
+
orgId: claims["org_id"],
|
|
17
|
+
role: claims["role"],
|
|
18
|
+
tokenType: claims["type"] === "service" ? "service" : "user"
|
|
19
|
+
});
|
|
20
|
+
return next();
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
requireAuth
|
|
24
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export { OidcHumanResolverConfig, createOidcHumanResolver } from './oidc-human.j
|
|
|
3
3
|
export { OidcAuthUrlConfig, PkceChallenge, buildOidcAuthUrl, generatePkce } from './pkce.js';
|
|
4
4
|
export { CliAuthResult, CliAuthStart, PollOptions, pollCliAuth, startCliAuth } from './cli-auth.js';
|
|
5
5
|
export { hashToken, looksLikeJwt, stripBearer } from './token.js';
|
|
6
|
+
export { JwtClaims, verifyHs256Jwt } from './jwt.js';
|
|
6
7
|
export { buildAccountProfileUrl, buildAuthLoginUrl, buildAuthLogoutUrl, buildPublicUrl, getAccountPublicUrl, getAppBasePath, getAuthPublicUrl, getPublicSiteUrl, normalizeUrlLike } from './url-helpers.js';
|
|
7
|
-
export { ZitadelClaims, ZitadelConfig, ZitadelIdentity, verifyZitadelToken } from './zitadel.js';
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
verifyHs256Jwt
|
|
3
|
+
} from "./chunk-TXDBQMTY.js";
|
|
4
4
|
import {
|
|
5
5
|
createOidcHumanResolver
|
|
6
6
|
} from "./chunk-6TUNNS2B.js";
|
|
7
7
|
import {
|
|
8
8
|
verifyOidcToken
|
|
9
9
|
} from "./chunk-BL74TFCV.js";
|
|
10
|
-
import {
|
|
11
|
-
buildOidcAuthUrl,
|
|
12
|
-
generatePkce
|
|
13
|
-
} from "./chunk-M7EACPIB.js";
|
|
14
10
|
import {
|
|
15
11
|
pollCliAuth,
|
|
16
12
|
startCliAuth
|
|
@@ -20,6 +16,10 @@ import {
|
|
|
20
16
|
looksLikeJwt,
|
|
21
17
|
stripBearer
|
|
22
18
|
} from "./chunk-VBIQJKUU.js";
|
|
19
|
+
import {
|
|
20
|
+
buildOidcAuthUrl,
|
|
21
|
+
generatePkce
|
|
22
|
+
} from "./chunk-34E5PYNA.js";
|
|
23
23
|
import {
|
|
24
24
|
buildAccountProfileUrl,
|
|
25
25
|
buildAuthLoginUrl,
|
|
@@ -49,6 +49,6 @@ export {
|
|
|
49
49
|
pollCliAuth,
|
|
50
50
|
startCliAuth,
|
|
51
51
|
stripBearer,
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
verifyHs256Jwt,
|
|
53
|
+
verifyOidcToken
|
|
54
54
|
};
|
package/dist/jwt.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface JwtClaims {
|
|
2
|
+
sub?: string;
|
|
3
|
+
exp?: number;
|
|
4
|
+
iat?: number;
|
|
5
|
+
org_id?: string;
|
|
6
|
+
role?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Verify an HS256 JWT using Node.js crypto.
|
|
12
|
+
* Returns decoded claims on success, null on any failure.
|
|
13
|
+
* Runtime: Node.js only (uses `crypto` module).
|
|
14
|
+
*/
|
|
15
|
+
declare function verifyHs256Jwt(token: string, secret: string): JwtClaims | null;
|
|
16
|
+
|
|
17
|
+
export { type JwtClaims, verifyHs256Jwt };
|
package/dist/jwt.js
ADDED
package/dist/pkce.d.ts
CHANGED
package/dist/pkce.js
CHANGED
package/dist/session.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baseworks/auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"./session": "./dist/session.js",
|
|
13
13
|
"./url-helpers": "./dist/url-helpers.js",
|
|
14
14
|
"./edge": "./dist/edge.js",
|
|
15
|
-
"./
|
|
15
|
+
"./jwt": "./dist/jwt.js",
|
|
16
|
+
"./hono": "./dist/hono.js"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"dist",
|
|
@@ -23,20 +24,25 @@
|
|
|
23
24
|
"typecheck": "tsc --noEmit"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"@baseworks/core": "
|
|
27
|
+
"@baseworks/core": "workspace:*",
|
|
27
28
|
"jose": "^6.2.3"
|
|
28
29
|
},
|
|
29
30
|
"peerDependencies": {
|
|
31
|
+
"hono": ">=4.0.0",
|
|
30
32
|
"next": ">=15.0.0"
|
|
31
33
|
},
|
|
32
34
|
"peerDependenciesMeta": {
|
|
33
35
|
"next": {
|
|
34
36
|
"optional": true
|
|
37
|
+
},
|
|
38
|
+
"hono": {
|
|
39
|
+
"optional": true
|
|
35
40
|
}
|
|
36
41
|
},
|
|
37
42
|
"devDependencies": {
|
|
38
43
|
"@baseworks/tsconfig": "workspace:*",
|
|
39
44
|
"@types/node": "^26.0.1",
|
|
45
|
+
"hono": "^4.12.27",
|
|
40
46
|
"tsup": "^8.5.1",
|
|
41
47
|
"typescript": "^5.6.0"
|
|
42
48
|
}
|
package/src/_internal.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared internal utilities — not exported from package public API.
|
|
3
|
+
*/
|
|
4
|
+
import { base64urlDecodeString } from '@baseworks/core'
|
|
5
|
+
|
|
6
|
+
export function parseJwtPayload(token: string): Record<string, unknown> | null {
|
|
7
|
+
try {
|
|
8
|
+
const part = token.split('.')[1]
|
|
9
|
+
if (!part) return null
|
|
10
|
+
return JSON.parse(base64urlDecodeString(part)) as Record<string, unknown>
|
|
11
|
+
} catch {
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
}
|
package/src/edge.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { headers } from "next/headers";
|
|
|
5
5
|
import { redirect } from "next/navigation";
|
|
6
6
|
import { getSessionFromCookies, type OidcSession } from "./session";
|
|
7
7
|
import { buildPublicUrl, normalizeUrlLike } from "./url-helpers";
|
|
8
|
+
import { parseJwtPayload } from "./_internal.js";
|
|
8
9
|
|
|
9
10
|
export type EdgeAuthProvider = "anonymous" | "cookie" | "pomerium";
|
|
10
11
|
|
|
@@ -24,22 +25,6 @@ function firstHeader(headerStore: Headers, names: string[]) {
|
|
|
24
25
|
return undefined;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
function decodeBase64Url(value: string) {
|
|
28
|
-
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
29
|
-
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=");
|
|
30
|
-
return Buffer.from(padded, "base64").toString("utf8");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function parseJwtPayload(token: string): Record<string, unknown> | null {
|
|
34
|
-
try {
|
|
35
|
-
const [, payload] = token.split(".");
|
|
36
|
-
if (!payload) return null;
|
|
37
|
-
return JSON.parse(decodeBase64Url(payload)) as Record<string, unknown>;
|
|
38
|
-
} catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
28
|
function stringClaim(claims: Record<string, unknown>, keys: string[]) {
|
|
44
29
|
for (const key of keys) {
|
|
45
30
|
const value = claims[key];
|
package/src/hono.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Context, Next } from 'hono'
|
|
2
|
+
import { verifyHs256Jwt } from './jwt.js'
|
|
3
|
+
|
|
4
|
+
export interface ServiceAuthUser {
|
|
5
|
+
userId: string
|
|
6
|
+
orgId?: string
|
|
7
|
+
role?: string
|
|
8
|
+
tokenType: 'user' | 'service'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module 'hono' {
|
|
12
|
+
interface ContextVariableMap {
|
|
13
|
+
auth: ServiceAuthUser
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hono middleware — verifies HS256 JWT from Authorization: Bearer header.
|
|
19
|
+
* Reads JWT_SECRET from process.env.
|
|
20
|
+
* Sets c.var.auth on success.
|
|
21
|
+
*/
|
|
22
|
+
export async function requireAuth(c: Context, next: Next) {
|
|
23
|
+
const header = c.req.header('Authorization')
|
|
24
|
+
if (!header?.startsWith('Bearer ')) return c.json({ error: 'Unauthorized' }, 401)
|
|
25
|
+
|
|
26
|
+
const token = header.slice(7)
|
|
27
|
+
const secret = process.env['JWT_SECRET']
|
|
28
|
+
if (!secret) return c.json({ error: 'JWT_SECRET not configured' }, 500)
|
|
29
|
+
|
|
30
|
+
const claims = verifyHs256Jwt(token, secret)
|
|
31
|
+
if (!claims) return c.json({ error: 'Unauthorized' }, 401)
|
|
32
|
+
|
|
33
|
+
c.set('auth', {
|
|
34
|
+
userId: String(claims['sub'] ?? ''),
|
|
35
|
+
orgId: claims['org_id'] as string | undefined,
|
|
36
|
+
role: claims['role'] as string | undefined,
|
|
37
|
+
tokenType: claims['type'] === 'service' ? 'service' : 'user',
|
|
38
|
+
})
|
|
39
|
+
return next()
|
|
40
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,10 @@ export type { CliAuthStart, CliAuthResult, PollOptions } from './cli-auth';
|
|
|
17
17
|
// Token utilities
|
|
18
18
|
export { hashToken, looksLikeJwt, stripBearer } from './token';
|
|
19
19
|
|
|
20
|
+
// HS256 JWT verification (Node.js)
|
|
21
|
+
export { verifyHs256Jwt } from './jwt';
|
|
22
|
+
export type { JwtClaims } from './jwt';
|
|
23
|
+
|
|
20
24
|
// URL helpers (auth/account public URLs, login/logout URL builders)
|
|
21
25
|
export {
|
|
22
26
|
normalizeUrlLike,
|
|
@@ -34,6 +38,3 @@ export {
|
|
|
34
38
|
// Use subpath imports: @baseworks/auth/session, @baseworks/auth/edge
|
|
35
39
|
// This keeps the main index Worker-safe (no next/server dependency).
|
|
36
40
|
|
|
37
|
-
// Legacy re-export — kept for backward compatibility, prefer verifyOidcToken
|
|
38
|
-
export { verifyZitadelToken } from './zitadel';
|
|
39
|
-
export type { ZitadelConfig, ZitadelIdentity, ZitadelClaims } from './zitadel';
|
package/src/jwt.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'crypto'
|
|
2
|
+
|
|
3
|
+
export interface JwtClaims {
|
|
4
|
+
sub?: string
|
|
5
|
+
exp?: number
|
|
6
|
+
iat?: number
|
|
7
|
+
org_id?: string
|
|
8
|
+
role?: string
|
|
9
|
+
type?: string
|
|
10
|
+
[key: string]: unknown
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Verify an HS256 JWT using Node.js crypto.
|
|
15
|
+
* Returns decoded claims on success, null on any failure.
|
|
16
|
+
* Runtime: Node.js only (uses `crypto` module).
|
|
17
|
+
*/
|
|
18
|
+
export function verifyHs256Jwt(token: string, secret: string): JwtClaims | null {
|
|
19
|
+
const parts = token.split('.')
|
|
20
|
+
if (parts.length !== 3) return null
|
|
21
|
+
|
|
22
|
+
const [header, payload, sig] = parts as [string, string, string]
|
|
23
|
+
const expected = createHmac('sha256', secret)
|
|
24
|
+
.update(`${header}.${payload}`)
|
|
25
|
+
.digest('base64url')
|
|
26
|
+
|
|
27
|
+
const sigBuf = Buffer.from(sig, 'base64url')
|
|
28
|
+
const expBuf = Buffer.from(expected, 'base64url')
|
|
29
|
+
if (sigBuf.length !== expBuf.length) return null
|
|
30
|
+
if (!timingSafeEqual(sigBuf, expBuf)) return null
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString()) as JwtClaims
|
|
34
|
+
if (claims.exp && claims.exp < Math.floor(Date.now() / 1000)) return null
|
|
35
|
+
return claims
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/pkce.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* PKCE (Proof Key for Code Exchange) utilities — RFC 7636.
|
|
3
3
|
* Runtime-agnostic: Workers, Node 20+, browser (Web Crypto API).
|
|
4
4
|
*/
|
|
5
|
+
import { base64urlEncode } from '@baseworks/core'
|
|
5
6
|
|
|
6
7
|
export interface PkceChallenge {
|
|
7
8
|
verifier: string;
|
|
@@ -29,14 +30,9 @@ export interface OidcAuthUrlConfig {
|
|
|
29
30
|
|
|
30
31
|
/** Generate a PKCE code_verifier + S256 code_challenge pair. */
|
|
31
32
|
export async function generatePkce(): Promise<PkceChallenge> {
|
|
32
|
-
const
|
|
33
|
-
crypto.
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const encoded = new TextEncoder().encode(verifier);
|
|
37
|
-
const digest = await crypto.subtle.digest('SHA-256', encoded);
|
|
38
|
-
const challenge = base64url(new Uint8Array(digest));
|
|
39
|
-
|
|
33
|
+
const verifier = base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
34
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
|
35
|
+
const challenge = base64urlEncode(new Uint8Array(digest));
|
|
40
36
|
return { verifier, challenge, method: 'S256' };
|
|
41
37
|
}
|
|
42
38
|
|
|
@@ -49,12 +45,12 @@ export function buildOidcAuthUrl(config: OidcAuthUrlConfig): string {
|
|
|
49
45
|
const scopes = (config.scopes ?? ['openid', 'email', 'profile']).join(' ');
|
|
50
46
|
|
|
51
47
|
const params = new URLSearchParams({
|
|
52
|
-
client_id:
|
|
53
|
-
redirect_uri:
|
|
54
|
-
response_type:
|
|
55
|
-
scope:
|
|
56
|
-
code_challenge:
|
|
57
|
-
code_challenge_method:
|
|
48
|
+
client_id: config.clientId,
|
|
49
|
+
redirect_uri: config.redirectUri,
|
|
50
|
+
response_type: 'code',
|
|
51
|
+
scope: scopes,
|
|
52
|
+
code_challenge: config.challenge,
|
|
53
|
+
code_challenge_method: 'S256',
|
|
58
54
|
});
|
|
59
55
|
|
|
60
56
|
if (config.state) params.set('state', config.state);
|
|
@@ -62,12 +58,3 @@ export function buildOidcAuthUrl(config: OidcAuthUrlConfig): string {
|
|
|
62
58
|
|
|
63
59
|
return `${issuer}/oauth/v2/authorize?${params.toString()}`;
|
|
64
60
|
}
|
|
65
|
-
|
|
66
|
-
// ─── helpers ────────────────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
function base64url(buf: Uint8Array): string {
|
|
69
|
-
return btoa(Array.from(buf, (b) => String.fromCharCode(b)).join(''))
|
|
70
|
-
.replace(/\+/g, '-')
|
|
71
|
-
.replace(/\//g, '_')
|
|
72
|
-
.replace(/=/g, '');
|
|
73
|
-
}
|
package/src/session.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
import { cookies } from "next/headers";
|
|
5
5
|
import type { NextRequest } from "next/server";
|
|
6
6
|
import { NextResponse } from "next/server";
|
|
7
|
+
import { base64urlEncode, base64urlDecodeString } from "@baseworks/core";
|
|
7
8
|
import { getAuthPublicUrl, normalizeUrlLike } from "./url-helpers";
|
|
9
|
+
import { generatePkce } from "./pkce.js";
|
|
8
10
|
|
|
9
11
|
type DiscoveryDocument = {
|
|
10
12
|
authorization_endpoint: string;
|
|
@@ -117,33 +119,8 @@ function resolvePublicRedirect(target: string, request: NextRequest) {
|
|
|
117
119
|
return new URL(target, request.url);
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
function base64UrlEncode(input: ArrayBuffer | Uint8Array | string) {
|
|
121
|
-
const buffer =
|
|
122
|
-
typeof input === "string"
|
|
123
|
-
? Buffer.from(input, "utf8")
|
|
124
|
-
: Buffer.from(input instanceof Uint8Array ? input : new Uint8Array(input));
|
|
125
|
-
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function base64UrlDecode(input: string) {
|
|
129
|
-
const padded = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
130
|
-
const remainder = padded.length % 4;
|
|
131
|
-
const normalized = remainder === 0 ? padded : `${padded}${"=".repeat(4 - remainder)}`;
|
|
132
|
-
return Buffer.from(normalized, "base64").toString("utf8");
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function sha256(input: string) {
|
|
136
|
-
return crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
122
|
function randomString() {
|
|
140
|
-
return
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
async function createPkcePair() {
|
|
144
|
-
const verifier = randomString();
|
|
145
|
-
const challenge = base64UrlEncode(await sha256(verifier));
|
|
146
|
-
return { challenge, verifier };
|
|
123
|
+
return base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
147
124
|
}
|
|
148
125
|
|
|
149
126
|
async function discoverOidc() {
|
|
@@ -154,17 +131,17 @@ async function discoverOidc() {
|
|
|
154
131
|
}
|
|
155
132
|
|
|
156
133
|
function parseJwtPayload(token: string): JwtPayload {
|
|
157
|
-
const
|
|
158
|
-
return JSON.parse(
|
|
134
|
+
const part = token.split(".")[1] ?? "";
|
|
135
|
+
return JSON.parse(base64urlDecodeString(part)) as JwtPayload;
|
|
159
136
|
}
|
|
160
137
|
|
|
161
138
|
function encodeSessionCookie(session: OidcSession) {
|
|
162
|
-
return
|
|
139
|
+
return base64urlEncode(new TextEncoder().encode(JSON.stringify(session)));
|
|
163
140
|
}
|
|
164
141
|
|
|
165
142
|
function parseSessionCookie(value: string): OidcSession | null {
|
|
166
143
|
try {
|
|
167
|
-
const session = JSON.parse(
|
|
144
|
+
const session = JSON.parse(base64urlDecodeString(value)) as OidcSession;
|
|
168
145
|
return session.isAuthenticated ? session : null;
|
|
169
146
|
} catch {
|
|
170
147
|
return null;
|
|
@@ -172,12 +149,12 @@ function parseSessionCookie(value: string): OidcSession | null {
|
|
|
172
149
|
}
|
|
173
150
|
|
|
174
151
|
function encodeTransactionCookie(transaction: OidcTransaction) {
|
|
175
|
-
return
|
|
152
|
+
return base64urlEncode(new TextEncoder().encode(JSON.stringify(transaction)));
|
|
176
153
|
}
|
|
177
154
|
|
|
178
155
|
function parseTransactionCookie(value: string): OidcTransaction | null {
|
|
179
156
|
try {
|
|
180
|
-
const transaction = JSON.parse(
|
|
157
|
+
const transaction = JSON.parse(base64urlDecodeString(value)) as OidcTransaction;
|
|
181
158
|
if (!transaction.codeVerifier || !transaction.returnTo) return null;
|
|
182
159
|
return transaction;
|
|
183
160
|
} catch {
|
|
@@ -232,7 +209,7 @@ function appendClientAuthentication(
|
|
|
232
209
|
clientSecret?: string,
|
|
233
210
|
) {
|
|
234
211
|
if (clientSecret) {
|
|
235
|
-
const basic =
|
|
212
|
+
const basic = btoa(`${clientId}:${clientSecret}`);
|
|
236
213
|
headers.set("authorization", `Basic ${basic}`);
|
|
237
214
|
return;
|
|
238
215
|
}
|
|
@@ -347,7 +324,7 @@ function clearTransientCookies(response: NextResponse, request: NextRequest) {
|
|
|
347
324
|
export async function buildAuthorizationRedirect(request: NextRequest) {
|
|
348
325
|
const discovery = await discoverOidc();
|
|
349
326
|
const clientId = getClientId();
|
|
350
|
-
const { challenge, verifier } = await
|
|
327
|
+
const { challenge, verifier } = await generatePkce();
|
|
351
328
|
const nonce = randomString();
|
|
352
329
|
const state = randomString();
|
|
353
330
|
const redirectUri = getRedirectUri(request);
|
package/dist/chunk-BMPRMOI7.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// src/zitadel.ts
|
|
2
|
-
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
3
|
-
function withoutTrailingSlash(v) {
|
|
4
|
-
return v.replace(/\/+$/, "");
|
|
5
|
-
}
|
|
6
|
-
async function verifyZitadelToken(token, config) {
|
|
7
|
-
const issuer = withoutTrailingSlash(config.issuer);
|
|
8
|
-
const jwks = createRemoteJWKSet(new URL(`${issuer}/oauth/v2/keys`));
|
|
9
|
-
const opts = config.audience ? { issuer, audience: config.audience } : { issuer };
|
|
10
|
-
const result = await jwtVerify(token, jwks, opts).catch(() => null);
|
|
11
|
-
if (!result) return null;
|
|
12
|
-
const payload = result.payload;
|
|
13
|
-
const subject = String(payload.sub ?? "");
|
|
14
|
-
if (!subject) return null;
|
|
15
|
-
const email = String(payload.email ?? payload.preferred_username ?? `${subject}@zitadel.local`);
|
|
16
|
-
const name = String(payload.name ?? payload.preferred_username ?? email);
|
|
17
|
-
const picture = payload.picture ? String(payload.picture) : void 0;
|
|
18
|
-
return { subject, issuer, email, name, picture };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export {
|
|
22
|
-
verifyZitadelToken
|
|
23
|
-
};
|
package/dist/zitadel.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
interface ZitadelConfig {
|
|
2
|
-
issuer: string;
|
|
3
|
-
audience?: string;
|
|
4
|
-
}
|
|
5
|
-
interface ZitadelClaims {
|
|
6
|
-
sub: string;
|
|
7
|
-
email?: string;
|
|
8
|
-
preferred_username?: string;
|
|
9
|
-
name?: string;
|
|
10
|
-
picture?: string;
|
|
11
|
-
exp?: number;
|
|
12
|
-
iss?: string;
|
|
13
|
-
aud?: string | string[];
|
|
14
|
-
}
|
|
15
|
-
interface ZitadelIdentity {
|
|
16
|
-
subject: string;
|
|
17
|
-
issuer: string;
|
|
18
|
-
email: string;
|
|
19
|
-
name: string;
|
|
20
|
-
picture?: string;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Verify a Zitadel JWT and return the identity.
|
|
24
|
-
* Returns null on any verification failure — never throws to the caller.
|
|
25
|
-
*/
|
|
26
|
-
declare function verifyZitadelToken(token: string, config: ZitadelConfig): Promise<ZitadelIdentity | null>;
|
|
27
|
-
|
|
28
|
-
export { type ZitadelClaims, type ZitadelConfig, type ZitadelIdentity, verifyZitadelToken };
|
package/dist/zitadel.js
DELETED
package/src/zitadel.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
|
-
|
|
3
|
-
export interface ZitadelConfig {
|
|
4
|
-
issuer: string;
|
|
5
|
-
audience?: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface ZitadelClaims {
|
|
9
|
-
sub: string;
|
|
10
|
-
email?: string;
|
|
11
|
-
preferred_username?: string;
|
|
12
|
-
name?: string;
|
|
13
|
-
picture?: string;
|
|
14
|
-
exp?: number;
|
|
15
|
-
iss?: string;
|
|
16
|
-
aud?: string | string[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Matches OidcIdentity in @baseworks/account — intentionally compatible.
|
|
20
|
-
export interface ZitadelIdentity {
|
|
21
|
-
subject: string;
|
|
22
|
-
issuer: string; // normalised, no trailing slash
|
|
23
|
-
email: string;
|
|
24
|
-
name: string;
|
|
25
|
-
picture?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function withoutTrailingSlash(v: string): string {
|
|
29
|
-
return v.replace(/\/+$/, '');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Verify a Zitadel JWT and return the identity.
|
|
34
|
-
* Returns null on any verification failure — never throws to the caller.
|
|
35
|
-
*/
|
|
36
|
-
export async function verifyZitadelToken(
|
|
37
|
-
token: string,
|
|
38
|
-
config: ZitadelConfig,
|
|
39
|
-
): Promise<ZitadelIdentity | null> {
|
|
40
|
-
const issuer = withoutTrailingSlash(config.issuer);
|
|
41
|
-
const jwks = createRemoteJWKSet(new URL(`${issuer}/oauth/v2/keys`));
|
|
42
|
-
const opts = config.audience ? { issuer, audience: config.audience } : { issuer };
|
|
43
|
-
|
|
44
|
-
const result = await jwtVerify(token, jwks, opts).catch(() => null);
|
|
45
|
-
if (!result) return null;
|
|
46
|
-
|
|
47
|
-
const payload = result.payload as ZitadelClaims;
|
|
48
|
-
const subject = String(payload.sub ?? '');
|
|
49
|
-
if (!subject) return null;
|
|
50
|
-
|
|
51
|
-
const email = String(payload.email ?? payload.preferred_username ?? `${subject}@zitadel.local`);
|
|
52
|
-
const name = String(payload.name ?? payload.preferred_username ?? email);
|
|
53
|
-
const picture = payload.picture ? String(payload.picture) : undefined;
|
|
54
|
-
|
|
55
|
-
return { subject, issuer, email, name, picture };
|
|
56
|
-
}
|