@aplons/auth 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -28
- package/dist/branding.d.ts +82 -0
- package/dist/branding.js +80 -0
- package/dist/client.d.ts +78 -55
- package/dist/client.js +173 -124
- package/dist/discovery.d.ts +11 -9
- package/dist/discovery.js +26 -26
- package/dist/errors.d.ts +12 -12
- package/dist/errors.js +49 -45
- package/dist/index.d.ts +12 -12
- package/dist/index.js +10 -10
- package/dist/next.d.ts +39 -37
- package/dist/next.js +259 -159
- package/dist/pkce.d.ts +27 -29
- package/dist/pkce.js +41 -43
- package/dist/types.d.ts +31 -32
- package/dist/verify.d.ts +14 -14
- package/dist/verify.js +47 -47
- package/package.json +8 -6
package/dist/next.js
CHANGED
|
@@ -1,262 +1,362 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @aplons/auth/next —
|
|
2
|
+
* @aplons/auth/next — the part you would otherwise write from scratch every
|
|
3
|
+
* time.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* `state`
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
8
|
-
*
|
|
5
|
+
* The core knows nothing about cookies; it has no idea where `verifier` and
|
|
6
|
+
* `state` live between two requests. But that is exactly where the work is,
|
|
7
|
+
* and exactly where the mistakes get made: the verifier in localStorage
|
|
8
|
+
* (every script on the page reads it), the state without `httpOnly`, a
|
|
9
|
+
* cookie without `secure` travelling over an open hotspot.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
+
* Here that happens once, correctly:
|
|
11
12
|
*
|
|
12
13
|
* app/api/auth/[...aplons]/route.ts
|
|
13
14
|
* ------------------------------------------------------------------
|
|
14
|
-
* import {
|
|
15
|
+
* import { createHandler } from "@aplons/auth/next";
|
|
15
16
|
*
|
|
16
|
-
* export const { GET, POST } =
|
|
17
|
+
* export const { GET, POST } = createHandler({
|
|
17
18
|
* issuer: process.env.APLONS_ISSUER!,
|
|
18
19
|
* clientId: process.env.APLONS_CLIENT_ID!,
|
|
19
20
|
* clientSecret: process.env.APLONS_CLIENT_SECRET,
|
|
20
21
|
* redirectUri: process.env.APLONS_REDIRECT_URI!,
|
|
21
22
|
* });
|
|
22
23
|
*
|
|
23
|
-
*
|
|
24
|
+
* That yields four routes: /api/auth/login, /callback, /logout, /me.
|
|
24
25
|
*
|
|
25
|
-
* Next.js
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* Next.js is a peer dependency and is deliberately not imported here — this
|
|
27
|
+
* module gets by with `Request` and `Response`, which exist everywhere. That
|
|
28
|
+
* makes it usable elsewhere too, and whoever only wants the core does not
|
|
29
|
+
* pull Next in with it.
|
|
29
30
|
*/
|
|
30
31
|
import { AplonsAuth } from "./client.js";
|
|
31
32
|
import { AplonsError } from "./errors.js";
|
|
32
|
-
const
|
|
33
|
+
const TRANSACTION_COOKIE = "aplons_tx";
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
+
* The four routes.
|
|
35
36
|
*
|
|
36
|
-
*
|
|
37
|
+
* Returned as `{ GET, POST }`, because that is what the App Router expects.
|
|
37
38
|
*/
|
|
38
|
-
export function
|
|
39
|
+
export function createHandler(options) {
|
|
39
40
|
const auth = new AplonsAuth(options);
|
|
40
|
-
const basePath = options.basePath ??
|
|
41
|
+
const basePath = options.basePath ?? deriveBasePath(options.redirectUri);
|
|
41
42
|
const cookieName = options.cookieName ?? "aplons_session";
|
|
42
43
|
async function GET(request) {
|
|
43
44
|
const url = new URL(request.url);
|
|
44
|
-
const
|
|
45
|
+
const action = url.pathname.slice(basePath.length).replace(/^\/+/, "");
|
|
45
46
|
/*
|
|
46
|
-
|
|
47
|
+
No error leaves this function.
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
A throw here would otherwise land as an unhandled rejection in the
|
|
50
|
+
customer's process — while testing, that simply killed the server with
|
|
51
|
+
nothing anywhere saying why. A login button that takes down the
|
|
52
|
+
application is the opposite of what a package is supposed to save you.
|
|
53
|
+
The message on AplonsError already says what to do; it comes out here
|
|
54
|
+
instead of in a stack trace.
|
|
54
55
|
*/
|
|
55
56
|
try {
|
|
56
|
-
switch (
|
|
57
|
+
switch (action) {
|
|
57
58
|
case "login":
|
|
58
|
-
return await
|
|
59
|
+
return await login(request, url);
|
|
59
60
|
case "callback":
|
|
60
|
-
return await
|
|
61
|
+
return await callback(request, url);
|
|
61
62
|
case "logout":
|
|
62
|
-
return await
|
|
63
|
+
return await logout(request);
|
|
63
64
|
case "me":
|
|
64
|
-
return await
|
|
65
|
+
return await me(request);
|
|
66
|
+
case "branding":
|
|
67
|
+
return await branding(request);
|
|
65
68
|
default:
|
|
66
69
|
return json({ error: "not_found" }, 404);
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
|
-
catch (
|
|
70
|
-
if (
|
|
71
|
-
return json({ error:
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof AplonsError) {
|
|
74
|
+
return json({ error: error.code, message: error.message }, 500);
|
|
72
75
|
}
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
console.error("[@aplons/auth]",
|
|
76
|
+
// Something unexpected: the message goes to the server log, not into
|
|
77
|
+
// the response — there it could give away internal paths.
|
|
78
|
+
console.error("[@aplons/auth]", error);
|
|
76
79
|
return json({
|
|
77
|
-
error: "
|
|
78
|
-
message: "
|
|
80
|
+
error: "unexpected",
|
|
81
|
+
message: "Something went wrong during login. See the server log.",
|
|
79
82
|
}, 500);
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
|
-
async function
|
|
83
|
-
const
|
|
85
|
+
async function login(request, url) {
|
|
86
|
+
const authorization = await auth.startLogin({
|
|
84
87
|
tenant: url.searchParams.get("tenant") ?? undefined,
|
|
85
|
-
|
|
88
|
+
forceLogin: url.searchParams.get("prompt") === "login",
|
|
86
89
|
});
|
|
87
|
-
const
|
|
90
|
+
const response = redirect(authorization.url);
|
|
88
91
|
/*
|
|
89
|
-
Verifier,
|
|
92
|
+
Verifier, state and nonce together in one short-lived cookie.
|
|
90
93
|
|
|
91
|
-
httpOnly
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
httpOnly so no script reads them; sameSite=lax because the return from
|
|
95
|
+
Aplons is a cross-site navigation and a `strict` cookie would not be
|
|
96
|
+
sent along with it — the flow would then break at the very last step.
|
|
97
|
+
Ten minutes: nobody needs longer for a login, and whatever sits open
|
|
98
|
+
longer is forgotten rather than in progress.
|
|
96
99
|
*/
|
|
97
|
-
|
|
98
|
-
v:
|
|
99
|
-
s:
|
|
100
|
-
n:
|
|
101
|
-
|
|
102
|
-
}), { maxAge: 600,
|
|
103
|
-
return
|
|
100
|
+
setCookie(response, TRANSACTION_COOKIE, JSON.stringify({
|
|
101
|
+
v: authorization.verifier,
|
|
102
|
+
s: authorization.state,
|
|
103
|
+
n: authorization.nonce,
|
|
104
|
+
next: url.searchParams.get("next") ?? options.afterLogin ?? "/",
|
|
105
|
+
}), { maxAge: 600, secure: isHttps(url) });
|
|
106
|
+
return response;
|
|
104
107
|
}
|
|
105
|
-
async function
|
|
106
|
-
const
|
|
107
|
-
if (!
|
|
108
|
-
return
|
|
109
|
-
"
|
|
110
|
-
"Browser begonnen wurde. Fang noch einmal an.");
|
|
108
|
+
async function callback(request, url) {
|
|
109
|
+
const raw = readCookie(request, TRANSACTION_COOKIE);
|
|
110
|
+
if (!raw) {
|
|
111
|
+
return errorResponse("The login is gone. That happens when it sat open for more than ten " +
|
|
112
|
+
"minutes, or was started in a different browser. Start again.");
|
|
111
113
|
}
|
|
112
|
-
let
|
|
114
|
+
let transaction;
|
|
113
115
|
try {
|
|
114
|
-
|
|
116
|
+
transaction = JSON.parse(raw);
|
|
115
117
|
}
|
|
116
118
|
catch {
|
|
117
|
-
return
|
|
119
|
+
return errorResponse("The login is unreadable. Start again.");
|
|
118
120
|
}
|
|
119
|
-
let
|
|
121
|
+
let session;
|
|
120
122
|
try {
|
|
121
|
-
|
|
123
|
+
session = await auth.completeLogin({
|
|
122
124
|
url,
|
|
123
|
-
verifier:
|
|
124
|
-
state:
|
|
125
|
-
nonce:
|
|
125
|
+
verifier: transaction.v,
|
|
126
|
+
state: transaction.s,
|
|
127
|
+
nonce: transaction.n,
|
|
126
128
|
});
|
|
127
129
|
}
|
|
128
|
-
catch (
|
|
129
|
-
return
|
|
130
|
+
catch (error) {
|
|
131
|
+
return errorResponse(error instanceof AplonsError ? error.message : "The login failed.");
|
|
130
132
|
}
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
// Relative targets only: a `next` parameter pointing at a foreign address
|
|
134
|
+
// would turn your own login into an open redirect for someone else.
|
|
135
|
+
const target = transaction.next.startsWith("/") && !transaction.next.startsWith("//")
|
|
136
|
+
? transaction.next
|
|
137
|
+
: "/";
|
|
138
|
+
const response = redirect(new URL(target, url).toString());
|
|
139
|
+
// The login is done; its cookie has no business sticking around.
|
|
140
|
+
clearCookie(response, TRANSACTION_COOKIE, isHttps(url));
|
|
141
|
+
await write(response, session, null, isHttps(url));
|
|
142
|
+
return response;
|
|
139
143
|
}
|
|
140
|
-
async function
|
|
141
|
-
const
|
|
144
|
+
async function logout(request) {
|
|
145
|
+
const stored = await readEntry(request);
|
|
146
|
+
const session = stored?.session ?? null;
|
|
142
147
|
const url = new URL(request.url);
|
|
143
|
-
if (
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
await auth.
|
|
148
|
+
if (session?.refreshToken) {
|
|
149
|
+
// The refresh token stays valid for thirty days unless somebody
|
|
150
|
+
// revokes it — a deleted cookie does not impress it.
|
|
151
|
+
await auth.revoke(session.refreshToken).catch(() => undefined);
|
|
147
152
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
/*
|
|
154
|
+
Und aus der eigenen Ablage heraus.
|
|
155
|
+
|
|
156
|
+
Das Cookie zu löschen nahm dem Browser nur den Schlüssel; der Eintrag
|
|
157
|
+
selbst — mitsamt Refresh-Token — blieb in der Datenbank liegen, für
|
|
158
|
+
jede Anmeldung eines jeden Kontos einer. `store.delete` stand in der
|
|
159
|
+
Beschreibung des Typs und wurde nirgends aufgerufen: wer die eigene
|
|
160
|
+
Ablage einbaute, sammelte abgemeldete Sitzungen, bis jemand nachsah.
|
|
161
|
+
*/
|
|
162
|
+
if (options.store && stored?.id) {
|
|
163
|
+
await options.store.delete(stored.id).catch(() => undefined);
|
|
164
|
+
}
|
|
165
|
+
const target = await auth.logoutUrl({
|
|
166
|
+
returnTo: options.afterLogout
|
|
167
|
+
? new URL(options.afterLogout, url).toString()
|
|
151
168
|
: undefined,
|
|
152
|
-
idToken:
|
|
169
|
+
idToken: session?.idToken,
|
|
153
170
|
});
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
if (options.
|
|
157
|
-
|
|
158
|
-
return
|
|
171
|
+
const response = redirect(target);
|
|
172
|
+
clearCookie(response, cookieName, isHttps(url));
|
|
173
|
+
if (options.store)
|
|
174
|
+
clearCookie(response, cookieName + "_id", isHttps(url));
|
|
175
|
+
return response;
|
|
159
176
|
}
|
|
160
|
-
async function
|
|
161
|
-
const
|
|
162
|
-
if (!
|
|
163
|
-
return json({
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
async function me(request) {
|
|
178
|
+
const state = await current(request);
|
|
179
|
+
if (!state)
|
|
180
|
+
return json({ authenticated: false }, 401);
|
|
181
|
+
return respond(json({ authenticated: true, account: state.session.claims ?? null }), state, request);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Das Erscheinungsbild des Mandanten, für die eigene Oberfläche.
|
|
185
|
+
*
|
|
186
|
+
* Braucht den Bereich `branding` in `scope` und einen Mandanten, der das
|
|
187
|
+
* Weitergeben in seinen Richtlinien erlaubt hat — sonst kommt die Absage von
|
|
188
|
+
* Aplons hier unverändert wieder heraus, damit die Ursache erkennbar bleibt
|
|
189
|
+
* statt als „irgendwas mit 500" zu enden.
|
|
190
|
+
*/
|
|
191
|
+
async function branding(request) {
|
|
192
|
+
const state = await current(request);
|
|
193
|
+
if (!state)
|
|
194
|
+
return json({ error: "not_authenticated" }, 401);
|
|
195
|
+
let payload;
|
|
196
|
+
try {
|
|
197
|
+
payload = json(await auth.branding(state.session.accessToken));
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (!(error instanceof AplonsError))
|
|
201
|
+
throw error;
|
|
202
|
+
payload = json({ error: error.code, message: error.message }, error.status ?? 500);
|
|
175
203
|
}
|
|
176
|
-
return
|
|
204
|
+
return respond(payload, state, request);
|
|
177
205
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Die Sitzung, wie sie *jetzt* gilt.
|
|
208
|
+
*
|
|
209
|
+
* Ist das Zugriffstoken abgelaufen, wird still erneuert. Ob das passiert
|
|
210
|
+
* ist, steht im Ergebnis: die erneuerte Sitzung muss in die Antwort dieses
|
|
211
|
+
* einen Aufrufs geschrieben werden, und nur der Aufrufer hält sie in der
|
|
212
|
+
* Hand.
|
|
213
|
+
*/
|
|
214
|
+
async function current(request) {
|
|
215
|
+
const stored = await readEntry(request);
|
|
216
|
+
if (!stored)
|
|
217
|
+
return null;
|
|
218
|
+
const { session, id } = stored;
|
|
219
|
+
if (session.accessTokenExpiresAt.getTime() >= Date.now() ||
|
|
220
|
+
!session.refreshToken) {
|
|
221
|
+
return { session, id, refreshed: false };
|
|
182
222
|
}
|
|
183
|
-
|
|
184
|
-
|
|
223
|
+
try {
|
|
224
|
+
return { session: await auth.refresh(session.refreshToken), id, refreshed: true };
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Eine Antwort, die eine unterwegs erneuerte Sitzung mitnimmt. */
|
|
231
|
+
async function respond(response, state, request) {
|
|
232
|
+
if (state.refreshed) {
|
|
233
|
+
await write(response, state.session, state.id, isHttps(new URL(request.url)));
|
|
234
|
+
}
|
|
235
|
+
return response;
|
|
236
|
+
}
|
|
237
|
+
async function readEntry(request) {
|
|
238
|
+
if (options.store) {
|
|
239
|
+
const id = readCookie(request, cookieName + "_id");
|
|
240
|
+
if (!id)
|
|
241
|
+
return null;
|
|
242
|
+
const session = await options.store.read(id);
|
|
243
|
+
return session ? { id, session: withDate(session) } : null;
|
|
244
|
+
}
|
|
245
|
+
const raw = readCookie(request, cookieName);
|
|
246
|
+
if (!raw)
|
|
185
247
|
return null;
|
|
186
248
|
try {
|
|
187
|
-
|
|
188
|
-
return { ...daten, accessTokenExpiresAt: new Date(daten.accessTokenExpiresAt) };
|
|
249
|
+
return { id: null, session: withDate(JSON.parse(raw)) };
|
|
189
250
|
}
|
|
190
251
|
catch {
|
|
191
252
|
return null;
|
|
192
253
|
}
|
|
193
254
|
}
|
|
194
|
-
async function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
255
|
+
async function readSession(request) {
|
|
256
|
+
return (await readEntry(request))?.session ?? null;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Die Sitzung in die Antwort schreiben — und in die eigene Ablage.
|
|
260
|
+
*
|
|
261
|
+
* `id` ist die Kennung, unter der sie dort schon liegt, oder `null` für eine
|
|
262
|
+
* neue Anmeldung. Sie wiederzuverwenden ist der Unterschied zwischen einer
|
|
263
|
+
* Zeile pro Sitzung und einer pro Erneuerung: erneuert wird alle zehn
|
|
264
|
+
* Minuten, und der alte Eintrag verschwand dabei nicht, sondern wurde nur
|
|
265
|
+
* unerreichbar.
|
|
266
|
+
*/
|
|
267
|
+
async function write(response, session, id, secure) {
|
|
268
|
+
if (options.store) {
|
|
269
|
+
const key = id ?? crypto.randomUUID();
|
|
270
|
+
await options.store.write(key, session);
|
|
271
|
+
setCookie(response, cookieName + "_id", key, { maxAge: 2592000, secure });
|
|
199
272
|
return;
|
|
200
273
|
}
|
|
201
|
-
|
|
274
|
+
setCookie(response, cookieName, JSON.stringify(session), {
|
|
202
275
|
maxAge: 2592000,
|
|
203
|
-
|
|
276
|
+
secure,
|
|
204
277
|
});
|
|
205
278
|
}
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
279
|
+
/**
|
|
280
|
+
* Read the session from one of your own routes.
|
|
281
|
+
*
|
|
282
|
+
* Erneuert nicht: eine eigene Route kann kein Cookie mitschicken, das sie
|
|
283
|
+
* nicht selbst schreibt. Wer sicher ein gültiges Token braucht, ruft `/me`
|
|
284
|
+
* auf oder erneuert selbst über `auth.refresh`.
|
|
285
|
+
*/
|
|
286
|
+
async function getSession(request) {
|
|
287
|
+
return readSession(request);
|
|
288
|
+
}
|
|
289
|
+
/** Das Erscheinungsbild, aus einer eigenen Route heraus. */
|
|
290
|
+
async function getBranding(request) {
|
|
291
|
+
const session = await readSession(request);
|
|
292
|
+
return session ? auth.branding(session.accessToken) : null;
|
|
209
293
|
}
|
|
210
|
-
return { GET, POST: GET, auth,
|
|
294
|
+
return { GET, POST: GET, auth, getSession, getBranding };
|
|
211
295
|
}
|
|
212
296
|
// ---------------------------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
|
|
297
|
+
/**
|
|
298
|
+
* Aus dem Abgelegten wieder eine Sitzung machen.
|
|
299
|
+
*
|
|
300
|
+
* `accessTokenExpiresAt` ist ein Datum. Durch JSON — im Cookie ebenso wie in
|
|
301
|
+
* einer fremden Ablage — kommt es als Zeichenkette zurück, und `.getTime()`
|
|
302
|
+
* gibt es darauf nicht: die Erneuerung lief dann in einen TypeError statt zu
|
|
303
|
+
* erneuern. Eine eigene Ablage, die Objekte behält, liefert dagegen ein
|
|
304
|
+
* richtiges Datum; beides muss hier durchkommen.
|
|
305
|
+
*/
|
|
306
|
+
function withDate(session) {
|
|
307
|
+
const expires = session.accessTokenExpiresAt;
|
|
308
|
+
return {
|
|
309
|
+
...session,
|
|
310
|
+
accessTokenExpiresAt: expires instanceof Date ? expires : new Date(expires),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function deriveBasePath(redirectUri) {
|
|
314
|
+
const path = new URL(redirectUri).pathname;
|
|
215
315
|
// .../api/auth/callback → .../api/auth
|
|
216
|
-
return
|
|
316
|
+
return path.replace(/\/callback\/?$/, "");
|
|
217
317
|
}
|
|
218
|
-
function
|
|
318
|
+
function isHttps(url) {
|
|
219
319
|
return url.protocol === "https:";
|
|
220
320
|
}
|
|
221
|
-
function
|
|
222
|
-
return new Response(null, { status: 302, headers: { location:
|
|
321
|
+
function redirect(target) {
|
|
322
|
+
return new Response(null, { status: 302, headers: { location: target } });
|
|
223
323
|
}
|
|
224
|
-
function json(
|
|
225
|
-
return new Response(JSON.stringify(
|
|
324
|
+
function json(data, status = 200) {
|
|
325
|
+
return new Response(JSON.stringify(data), {
|
|
226
326
|
status,
|
|
227
327
|
headers: { "content-type": "application/json" },
|
|
228
328
|
});
|
|
229
329
|
}
|
|
230
|
-
function
|
|
231
|
-
return json({ error: "
|
|
330
|
+
function errorResponse(message) {
|
|
331
|
+
return json({ error: "login_failed", message }, 400);
|
|
232
332
|
}
|
|
233
|
-
function
|
|
234
|
-
const
|
|
235
|
-
`${name}=${encodeURIComponent(
|
|
333
|
+
function setCookie(response, name, value, options) {
|
|
334
|
+
const parts = [
|
|
335
|
+
`${name}=${encodeURIComponent(value)}`,
|
|
236
336
|
"Path=/",
|
|
237
337
|
"HttpOnly",
|
|
238
338
|
"SameSite=Lax",
|
|
239
339
|
`Max-Age=${options.maxAge}`,
|
|
240
340
|
];
|
|
241
|
-
//
|
|
242
|
-
// http://localhost
|
|
243
|
-
if (options.
|
|
244
|
-
|
|
245
|
-
|
|
341
|
+
// No Secure without HTTPS — otherwise the cookie would never arrive during
|
|
342
|
+
// development on http://localhost, and nobody would find the reason.
|
|
343
|
+
if (options.secure)
|
|
344
|
+
parts.push("Secure");
|
|
345
|
+
response.headers.append("set-cookie", parts.join("; "));
|
|
246
346
|
}
|
|
247
|
-
function
|
|
248
|
-
|
|
347
|
+
function clearCookie(response, name, secure) {
|
|
348
|
+
setCookie(response, name, "", { maxAge: 0, secure });
|
|
249
349
|
}
|
|
250
|
-
function
|
|
251
|
-
const
|
|
252
|
-
if (!
|
|
350
|
+
function readCookie(request, name) {
|
|
351
|
+
const header = request.headers.get("cookie");
|
|
352
|
+
if (!header)
|
|
253
353
|
return null;
|
|
254
|
-
for (const
|
|
255
|
-
const
|
|
256
|
-
if (
|
|
354
|
+
for (const part of header.split(";")) {
|
|
355
|
+
const separator = part.indexOf("=");
|
|
356
|
+
if (separator < 0)
|
|
257
357
|
continue;
|
|
258
|
-
if (
|
|
259
|
-
return decodeURIComponent(
|
|
358
|
+
if (part.slice(0, separator).trim() === name) {
|
|
359
|
+
return decodeURIComponent(part.slice(separator + 1));
|
|
260
360
|
}
|
|
261
361
|
}
|
|
262
362
|
return null;
|
package/dist/pkce.d.ts
CHANGED
|
@@ -1,48 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PKCE —
|
|
3
|
-
* angefordert hat.
|
|
2
|
+
* PKCE — proof that whoever redeems the code is the one who asked for it.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The code comes back through a redirect URL, which puts it in the browser
|
|
5
|
+
* history, in the log of every proxy in between, and in the referer of the
|
|
6
|
+
* next page. Without PKCE, anyone who reads it there can redeem it. With
|
|
7
|
+
* PKCE they also need the verifier, and that never left the browser.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Everything here runs on built-in Web Crypto — no package, no dependency.
|
|
10
|
+
* Available in Node 18+, in every browser, and at the edge.
|
|
12
11
|
*/
|
|
13
12
|
/**
|
|
14
|
-
* Base64
|
|
13
|
+
* Base64 without the three characters that mean something else in a URL.
|
|
15
14
|
*
|
|
16
|
-
* `+`
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* außer nach seiner Ursache.
|
|
15
|
+
* `+` becomes a space in form encoding, `/` separates path segments, and `=`
|
|
16
|
+
* separates a parameter from its value. A verifier containing them arrives
|
|
17
|
+
* altered at the other end and no longer matches its challenge — a failure
|
|
18
|
+
* that surfaces as `invalid_grant` and looks like anything but its cause.
|
|
21
19
|
*/
|
|
22
|
-
export declare function base64url(
|
|
20
|
+
export declare function base64url(data: ArrayBuffer | Uint8Array): string;
|
|
23
21
|
/**
|
|
24
|
-
*
|
|
22
|
+
* A verifier: 32 bytes of randomness, base64url — 43 characters.
|
|
25
23
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* That is the lower bound from RFC 7636 and it is also enough: 256 bits of
|
|
25
|
+
* randomness cannot be guessed. Going up to the 128-character limit would
|
|
26
|
+
* add nothing.
|
|
29
27
|
*/
|
|
30
28
|
export declare function createVerifier(): string;
|
|
31
|
-
/**
|
|
29
|
+
/** What goes into the authorization URL: the hash, never the verifier. */
|
|
32
30
|
export declare function createChallenge(verifier: string): Promise<string>;
|
|
33
31
|
/**
|
|
34
|
-
*
|
|
32
|
+
* The value that guards against cross-site request forgery.
|
|
35
33
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* Without it someone could start a login with *their* account and hand the
|
|
35
|
+
* victim the finished redirect URL; the victim would end up signed into a
|
|
36
|
+
* stranger's account and file their data there.
|
|
39
37
|
*/
|
|
40
38
|
export declare function createState(): string;
|
|
41
39
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* Compare two strings without revealing, through timing, where they start
|
|
41
|
+
* to differ.
|
|
44
42
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* For the state this is strictly more than necessary — but the function is
|
|
44
|
+
* also here for whoever applies it to something more sensitive.
|
|
47
45
|
*/
|
|
48
|
-
export declare function
|
|
46
|
+
export declare function timingSafeEqual(a: string, b: string): boolean;
|