@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/dist/client.js CHANGED
@@ -1,19 +1,18 @@
1
1
  /**
2
- * Die Anbindung an Aplons.
2
+ * The connection to Aplons.
3
3
  *
4
- * Vier Schritte, mehr ist es nicht: Anmeldung starten, Rückkehr entgegen-
5
- * nehmen, Token erneuern, abmelden. Dazu das Prüfen eines Tokens für den,
6
- * der eine API dahinter absichert.
4
+ * Four steps, no more: start the login, complete it, refresh the tokens, log
5
+ * out. Plus verifying a token, for whoever has an API behind it.
7
6
  *
8
- * Alles, was der Ablauf sonst noch braucht welcher Endpunkt wo liegt,
9
- * welche Schlüssel gerade gültig sind —, holt sich das Paket selbst. Wer nur
10
- * `issuer` und `clientId` kennt, ist fertig.
7
+ * Everything else the flow needs which endpoint lives where, which keys are
8
+ * currently valid the package fetches itself. If you know `issuer` and
9
+ * `clientId`, you are done.
11
10
  */
12
- import { AplonsError, ausAntwort } from "./errors.js";
13
- import { createChallenge, createState, createVerifier, gleich } from "./pkce.js";
14
- import { holeMetadaten } from "./discovery.js";
15
- import { pruefeIdToken, pruefeZugriffstoken, schluesselFuer } from "./verify.js";
16
- const STANDARD_SCOPE = ["openid", "profile", "email"];
11
+ import { AplonsError, fromResponse } from "./errors.js";
12
+ import { createChallenge, createState, createVerifier, timingSafeEqual } from "./pkce.js";
13
+ import { fetchMetadata } from "./discovery.js";
14
+ import { jwksFor, verifyAccessToken, verifyIdToken } from "./verify.js";
15
+ const DEFAULT_SCOPE = ["openid", "profile", "email"];
17
16
  export class AplonsAuth {
18
17
  issuer;
19
18
  clientId;
@@ -21,46 +20,48 @@ export class AplonsAuth {
21
20
  scope;
22
21
  #clientSecret;
23
22
  #fetch;
24
- #metadaten;
23
+ #metadata;
25
24
  constructor(options) {
26
- if (!options.issuer)
27
- throw new AplonsError({ code: "config", message: "issuer fehlt." });
28
- if (!options.clientId)
29
- throw new AplonsError({ code: "config", message: "clientId fehlt." });
25
+ if (!options.issuer) {
26
+ throw new AplonsError({ code: "config", message: "issuer is missing." });
27
+ }
28
+ if (!options.clientId) {
29
+ throw new AplonsError({ code: "config", message: "clientId is missing." });
30
+ }
30
31
  if (!options.redirectUri) {
31
- throw new AplonsError({ code: "config", message: "redirectUri fehlt." });
32
+ throw new AplonsError({ code: "config", message: "redirectUri is missing." });
32
33
  }
33
- // Ein Schrägstrich am Ende erzeugt sonst `https://auth.example.com//oauth/…`.
34
+ // A trailing slash would otherwise produce `https://auth.example.com//oauth/…`.
34
35
  this.issuer = options.issuer.replace(/\/+$/, "");
35
36
  this.clientId = options.clientId;
36
37
  this.redirectUri = options.redirectUri;
37
- this.scope = options.scope ?? STANDARD_SCOPE;
38
+ this.scope = options.scope ?? DEFAULT_SCOPE;
38
39
  this.#clientSecret = options.clientSecret;
39
40
  this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
40
41
  }
41
42
  /**
42
- * Die Endpunkte, einmal geholt und dann behalten.
43
+ * The endpoints, fetched once and then kept.
43
44
  *
44
- * Als Versprechen zwischengespeichert, nicht als Ergebnis: sonst holen
45
- * zehn gleichzeitige Anfragen beim Start zehnmal dieselbe Datei.
45
+ * Cached as a promise, not as a result: otherwise ten concurrent requests
46
+ * at startup fetch the same document ten times.
46
47
  */
47
- metadaten() {
48
- this.#metadaten ??= holeMetadaten(this.issuer, this.#fetch);
49
- return this.#metadaten;
48
+ metadata() {
49
+ this.#metadata ??= fetchMetadata(this.issuer, this.#fetch);
50
+ return this.#metadata;
50
51
  }
51
52
  /**
52
- * Schritt 1: Wohin der Browser geschickt wird.
53
+ * Step 1: where to send the browser.
53
54
  *
54
- * `verifier` und `state` müssen bis zur Rückkehr aufbewahrt werden — in
55
- * einem kurzlebigen, `httpOnly`-Cookie, nicht im localStorage: was dort
56
- * liegt, liest jedes Skript auf der Seite.
55
+ * `verifier`, `state` and `nonce` have to survive until the callback — in a
56
+ * short-lived, `httpOnly` cookie, not in localStorage: whatever sits there
57
+ * is readable by every script on the page.
57
58
  */
58
- async start(options) {
59
- const metadaten = await this.metadaten();
59
+ async startLogin(options) {
60
+ const metadata = await this.metadata();
60
61
  const verifier = createVerifier();
61
62
  const state = createState();
62
63
  const nonce = createState();
63
- const url = new URL(metadaten.authorization_endpoint);
64
+ const url = new URL(metadata.authorization_endpoint);
64
65
  url.searchParams.set("response_type", "code");
65
66
  url.searchParams.set("client_id", this.clientId);
66
67
  url.searchParams.set("redirect_uri", this.redirectUri);
@@ -68,31 +69,31 @@ export class AplonsAuth {
68
69
  url.searchParams.set("state", state);
69
70
  url.searchParams.set("nonce", nonce);
70
71
  url.searchParams.set("code_challenge", await createChallenge(verifier));
71
- // S256 oder gar nicht: plain" legt den Verifier in denselben URL wie den
72
- // Code und schützt damit vor nichts.
72
+ // S256 or nothing: "plain" puts the verifier in the same URL as the code
73
+ // and therefore protects against nothing.
73
74
  url.searchParams.set("code_challenge_method", "S256");
74
- if (options?.erneutAnmelden)
75
+ if (options?.forceLogin)
75
76
  url.searchParams.set("prompt", "login");
76
77
  if (options?.tenant)
77
78
  url.searchParams.set("tenant", options.tenant);
78
- for (const [name, wert] of Object.entries(options?.zusatz ?? {})) {
79
- url.searchParams.set(name, wert);
79
+ for (const [name, value] of Object.entries(options?.extraParams ?? {})) {
80
+ url.searchParams.set(name, value);
80
81
  }
81
82
  return { url: url.toString(), verifier, state, nonce };
82
83
  }
83
84
  /**
84
- * Schritt 2: Die Rückkehr.
85
+ * Step 2: the callback.
85
86
  *
86
- * Nimmt den vollständigen URL entgegen, mit dem der Browser zurückkam, und
87
- * die beiden Werte aus Schritt 1.
87
+ * Takes the full URL the browser came back with, plus the values from
88
+ * step 1.
88
89
  */
89
- async rueckkehr(options) {
90
+ async completeLogin(options) {
90
91
  const url = typeof options.url === "string" ? new URL(options.url) : options.url;
91
- const fehler = url.searchParams.get("error");
92
- if (fehler) {
92
+ const error = url.searchParams.get("error");
93
+ if (error) {
93
94
  throw new AplonsError({
94
- code: fehler,
95
- message: `Die Anmeldung wurde abgebrochen (${fehler}).` +
95
+ code: error,
96
+ message: `The login was cancelled (${error}).` +
96
97
  (url.searchParams.get("error_description")
97
98
  ? ` ${url.searchParams.get("error_description")}`
98
99
  : ""),
@@ -101,148 +102,196 @@ export class AplonsAuth {
101
102
  const state = url.searchParams.get("state");
102
103
  const code = url.searchParams.get("code");
103
104
  /*
104
- Der State wird geprüft, bevor irgendetwas mit dem Code geschieht.
105
+ The state is checked before anything happens with the code.
105
106
 
106
- Ohne diese Prüfung könnte jemand einen Anmeldevorgang mit seinem eigenen
107
- Konto beginnen und dem Opfer den fertigen Rückkehr-URL unterschieben
108
- das Opfer wäre danach im fremden Konto angemeldet und legte dort seine
109
- Daten ab.
107
+ Without this check someone could start a login with their own account
108
+ and hand the victim the finished redirect URL the victim would end up
109
+ signed into a stranger's account and file their data there.
110
110
  */
111
- if (!state || !gleich(state, options.state)) {
111
+ if (!state || !timingSafeEqual(state, options.state)) {
112
112
  throw new AplonsError({
113
113
  code: "state_mismatch",
114
- message: "Der state stimmt nicht. Entweder ist der Anmeldevorgang zu alt, " +
115
- "oder der Aufruf kam nicht von der Anmeldung, die diese Anwendung " +
116
- "gestartet hat.",
114
+ message: "The state does not match. Either the login is too old, or the " +
115
+ "request did not come from the login this application started.",
117
116
  });
118
117
  }
119
118
  if (!code) {
120
119
  throw new AplonsError({
121
120
  code: "missing_code",
122
- message: "Im Rückkehr-URL steht kein code.",
121
+ message: "There is no code in the callback URL.",
123
122
  });
124
123
  }
125
- const antwort = await this.#token({
124
+ const response = await this.#tokenRequest({
126
125
  grant_type: "authorization_code",
127
126
  code,
128
127
  redirect_uri: this.redirectUri,
129
128
  code_verifier: options.verifier,
130
129
  });
131
- return this.#alsSitzung(antwort, options.nonce);
130
+ return this.#toSession(response, options.nonce);
132
131
  }
133
- /** Schritt 3: Ein abgelaufenes Zugriffstoken gegen ein frisches tauschen. */
134
- async erneuern(refreshToken) {
135
- const antwort = await this.#token({
132
+ /** Step 3: trade an expired access token for a fresh one. */
133
+ async refresh(refreshToken) {
134
+ const response = await this.#tokenRequest({
136
135
  grant_type: "refresh_token",
137
136
  refresh_token: refreshToken,
138
137
  });
139
- return this.#alsSitzung(antwort);
138
+ return this.#toSession(response);
140
139
  }
141
140
  /**
142
- * Schritt 4: Abmeldenbei Aplons, nicht nur hier.
141
+ * Step 4: log out at Aplons, not just here.
143
142
  *
144
- * Nur die eigene Sitzung zu löschen genügt nicht: die Sitzung bei Aplons
145
- * bliebe bestehen, und die nächste Anmeldung liefe ohne Passwort durch.
146
- * Auf einem geteilten Rechner ist das der Unterschied zwischen abgemeldet
147
- * und scheinbar abgemeldet.
143
+ * Clearing your own session is not enough: the session at Aplons would
144
+ * survive, and the next login would sail through without a password. On a
145
+ * shared machine that is the difference between signed out and apparently
146
+ * signed out.
148
147
  */
149
- async abmeldeUrl(options) {
150
- const metadaten = await this.metadaten();
151
- const url = new URL(metadaten.end_session_endpoint);
148
+ async logoutUrl(options) {
149
+ const metadata = await this.metadata();
150
+ const url = new URL(required(metadata.end_session_endpoint, "end_session_endpoint", "Logging out"));
152
151
  url.searchParams.set("client_id", this.clientId);
153
- if (options?.danach)
154
- url.searchParams.set("post_logout_redirect_uri", options.danach);
152
+ if (options?.returnTo) {
153
+ url.searchParams.set("post_logout_redirect_uri", options.returnTo);
154
+ }
155
155
  if (options?.idToken)
156
156
  url.searchParams.set("id_token_hint", options.idToken);
157
157
  return url.toString();
158
158
  }
159
159
  /**
160
- * Ein Zugriffstoken prüfenfür den, der eine eigene API dahinter hat.
160
+ * Verify an access token for whoever has their own API behind it.
161
161
  *
162
- * Geprüft wird gegen die öffentlichen Schlüssel von Aplons, ohne Rückfrage
163
- * bei jedem Aufruf: Unterschrift, Aussteller und Ablauf. Das ist der
164
- * Unterschied zwischen „das Token sieht echt aus" und „das Token ist echt".
162
+ * Checked against the public keys of Aplons, without a round trip on every
163
+ * call: signature, issuer and expiry. That is the difference between "the
164
+ * token looks real" and "the token is real".
165
165
  */
166
- async pruefeToken(token) {
167
- const metadaten = await this.metadaten();
168
- return pruefeZugriffstoken(token, {
169
- issuer: metadaten.issuer,
170
- schluessel: schluesselFuer(metadaten.jwks_uri),
166
+ async verifyAccessToken(token) {
167
+ const metadata = await this.metadata();
168
+ return verifyAccessToken(token, {
169
+ issuer: metadata.issuer,
170
+ keys: jwksFor(metadata.jwks_uri),
171
171
  });
172
172
  }
173
- /** Die Angaben zum angemeldeten Konto, so weit die Bereiche es hergeben. */
174
- async profil(accessToken) {
175
- const metadaten = await this.metadaten();
176
- const antwort = await this.#fetch(metadaten.userinfo_endpoint, {
173
+ /**
174
+ * Das Erscheinungsbild des Mandanten — Logo, Farben, Eckenradius.
175
+ *
176
+ * Ein Aufruf, damit eine eingebundene Oberfläche aussieht wie die Firma,
177
+ * für die sie gebaut ist, statt deren Logo ein zweites Mal zu pflegen:
178
+ *
179
+ * const branding = await auth.branding(session.accessToken);
180
+ *
181
+ * Welcher Mandant, steht im Token — nicht im Aufruf. Ein Konto bekommt
182
+ * damit immer genau das Erscheinungsbild, das es auf der Anmeldeseite auch
183
+ * gesehen hat.
184
+ *
185
+ * Es lohnt sich, das Ergebnis zwischenzuspeichern: ein Logo wechselt selten,
186
+ * und eine Seite, die es bei jedem Aufruf neu holt, wartet dafür jedes Mal
187
+ * auf eine Antwort aus dem Netz.
188
+ *
189
+ * Zwei Absagen sind möglich und meinen Verschiedenes: `insufficient_scope`
190
+ * heißt, das Token trägt den Bereich `branding` nicht — dann fehlt er in den
191
+ * `scope`s dieser Anwendung. `branding_not_shared` heißt, der Mandant gibt
192
+ * sein Erscheinungsbild nicht heraus; das steht in seinen Richtlinien und
193
+ * ist ab Werk aus.
194
+ */
195
+ async branding(accessToken) {
196
+ const metadata = await this.metadata();
197
+ const response = await this.#fetch(
198
+ // Ältere Server kennen den Eintrag im Discovery-Dokument noch nicht.
199
+ // Die Adresse selbst gibt es dort aber schon, deshalb der Rückfall statt
200
+ // einer Fehlermeldung, die niemandem hilft.
201
+ metadata.branding_endpoint ?? `${this.issuer}/api/oauth/branding`, { headers: { authorization: `Bearer ${accessToken}` } });
202
+ if (!response.ok)
203
+ throw await fromResponse(response, "Loading the branding");
204
+ return (await response.json());
205
+ }
206
+ /** The details of the signed-in account, as far as the scopes allow. */
207
+ async userInfo(accessToken) {
208
+ const metadata = await this.metadata();
209
+ const response = await this.#fetch(metadata.userinfo_endpoint, {
177
210
  headers: { authorization: `Bearer ${accessToken}` },
178
211
  });
179
- if (!antwort.ok)
180
- throw await ausAntwort(antwort, "Das Profil zu laden");
181
- return (await antwort.json());
212
+ if (!response.ok)
213
+ throw await fromResponse(response, "Loading the profile");
214
+ return (await response.json());
182
215
  }
183
216
  /**
184
- * Ein Refresh-Token entwerten.
217
+ * Revoke a refresh token.
185
218
  *
186
- * Gehört zum Abmelden dazu: ein Token, das noch dreißig Tage gilt, wird
187
- * vom Löschen des Cookies nicht ungültig.
219
+ * Part of logging out: a token still valid for thirty days does not become
220
+ * invalid because a cookie was deleted.
188
221
  */
189
- async widerrufen(refreshToken) {
190
- const metadaten = await this.metadaten();
191
- const koerper = new URLSearchParams({
222
+ async revoke(refreshToken) {
223
+ const metadata = await this.metadata();
224
+ const body = new URLSearchParams({
192
225
  token: refreshToken,
193
226
  token_type_hint: "refresh_token",
194
227
  client_id: this.clientId,
195
228
  });
196
229
  if (this.#clientSecret)
197
- koerper.set("client_secret", this.#clientSecret);
198
- const antwort = await this.#fetch(metadaten.revocation_endpoint, {
230
+ body.set("client_secret", this.#clientSecret);
231
+ const response = await this.#fetch(required(metadata.revocation_endpoint, "revocation_endpoint", "Revoking the token"), {
199
232
  method: "POST",
200
233
  headers: { "content-type": "application/x-www-form-urlencoded" },
201
- body: koerper,
234
+ body,
202
235
  });
203
- // RFC 7009 verlangt 200 auch für ein Token, das es nie gab damit
204
- // niemand über den Statuscode herausfindet, welche Token gültig sind.
205
- if (!antwort.ok)
206
- throw await ausAntwort(antwort, "Das Token zu widerrufen");
236
+ // RFC 7009 demands 200 even for a token that never existedso nobody
237
+ // can find out from the status code which tokens are valid.
238
+ if (!response.ok)
239
+ throw await fromResponse(response, "Revoking the token");
207
240
  }
208
- async #token(felder) {
209
- const metadaten = await this.metadaten();
210
- const koerper = new URLSearchParams({ ...felder, client_id: this.clientId });
241
+ async #tokenRequest(fields) {
242
+ const metadata = await this.metadata();
243
+ const body = new URLSearchParams({ ...fields, client_id: this.clientId });
211
244
  if (this.#clientSecret)
212
- koerper.set("client_secret", this.#clientSecret);
213
- const antwort = await this.#fetch(metadaten.token_endpoint, {
245
+ body.set("client_secret", this.#clientSecret);
246
+ const response = await this.#fetch(metadata.token_endpoint, {
214
247
  method: "POST",
215
248
  headers: {
216
249
  "content-type": "application/x-www-form-urlencoded",
217
250
  accept: "application/json",
218
251
  },
219
- body: koerper,
252
+ body,
220
253
  });
221
- if (!antwort.ok) {
222
- const istErneuerung = felder.grant_type === "refresh_token";
223
- throw await ausAntwort(antwort, istErneuerung ? "Das Erneuern der Anmeldung" : "Der Tausch des Anmeldecodes", istErneuerung ? "refresh" : "code");
254
+ if (!response.ok) {
255
+ const isRefresh = fields.grant_type === "refresh_token";
256
+ throw await fromResponse(response, isRefresh ? "Refreshing the session" : "Exchanging the authorization code", isRefresh ? "refresh" : "code");
224
257
  }
225
- return (await antwort.json());
258
+ return (await response.json());
226
259
  }
227
- async #alsSitzung(antwort, nonce) {
228
- const metadaten = await this.metadaten();
229
- const claims = antwort.id_token
230
- ? await pruefeIdToken(antwort.id_token, {
231
- issuer: metadaten.issuer,
260
+ async #toSession(response, nonce) {
261
+ const metadata = await this.metadata();
262
+ const claims = response.id_token
263
+ ? await verifyIdToken(response.id_token, {
264
+ issuer: metadata.issuer,
232
265
  audience: this.clientId,
233
266
  nonce,
234
- schluessel: schluesselFuer(metadaten.jwks_uri),
267
+ keys: jwksFor(metadata.jwks_uri),
235
268
  })
236
269
  : undefined;
237
270
  return {
238
- accessToken: antwort.access_token,
239
- refreshToken: antwort.refresh_token,
240
- // Aus der Dauer sofort einen Zeitpunkt: eine Dauer ist ab dem Moment
241
- // falsch, in dem man sie irgendwo ablegt.
242
- accessTokenExpiresAt: new Date(Date.now() + antwort.expires_in * 1000),
243
- idToken: antwort.id_token,
244
- scope: antwort.scope ? antwort.scope.split(" ").filter(Boolean) : [],
271
+ accessToken: response.access_token,
272
+ refreshToken: response.refresh_token,
273
+ // Turn the duration into a point in time right away: a duration is
274
+ // wrong the moment you store it somewhere.
275
+ accessTokenExpiresAt: new Date(Date.now() + response.expires_in * 1000),
276
+ idToken: response.id_token,
277
+ scope: response.scope ? response.scope.split(" ").filter(Boolean) : [],
245
278
  claims,
246
279
  };
247
280
  }
248
281
  }
282
+ /**
283
+ * Einen Eintrag aus dem Discovery-Dokument holen, den dieser Aufruf braucht.
284
+ *
285
+ * Fehlt er, sagt der Fehler *welcher* und *wofür* — statt eines nackten
286
+ * „Invalid URL" aus dem URL-Konstruktor, das beides verschweigt.
287
+ */
288
+ function required(value, field, what) {
289
+ if (!value) {
290
+ throw new AplonsError({
291
+ code: "invalid_metadata",
292
+ message: `${what} is not possible: the server does not list a ${field} in ` +
293
+ "its /.well-known/openid-configuration.",
294
+ });
295
+ }
296
+ return value;
297
+ }
@@ -1,20 +1,22 @@
1
1
  /**
2
- * Wo bei Aplons was liegtgefragt, nicht geraten.
2
+ * Where things live at Aplonsasked for, not guessed.
3
3
  *
4
- * Die Endpunkte ließen sich auch fest eintragen; genau das ist in diesem
5
- * Projekt schon schiefgegangen, als eine handgeschriebene Liste von Bereichen
6
- * neben der echten herlief und Anwendungen die falschen anforderten. Wer
7
- * fragt, bekommt immer die Antwort von heute.
4
+ * The endpoints could be hard-coded; that is exactly what went wrong in this
5
+ * project once already, when a hand-written list of scopes drifted away from
6
+ * the real one and applications requested the wrong things. Whoever asks
7
+ * gets today's answer.
8
8
  */
9
- export type Metadaten = {
9
+ export type Metadata = {
10
10
  issuer: string;
11
11
  authorization_endpoint: string;
12
12
  token_endpoint: string;
13
13
  userinfo_endpoint: string;
14
14
  jwks_uri: string;
15
- revocation_endpoint: string;
16
- end_session_endpoint: string;
15
+ revocation_endpoint?: string;
16
+ end_session_endpoint?: string;
17
+ /** Das Erscheinungsbild des Mandanten. Eigene Zutat, kein OpenID Connect. */
18
+ branding_endpoint?: string;
17
19
  scopes_supported?: string[];
18
20
  code_challenge_methods_supported?: string[];
19
21
  };
20
- export declare function holeMetadaten(issuer: string, fetchImpl: typeof globalThis.fetch): Promise<Metadaten>;
22
+ export declare function fetchMetadata(issuer: string, fetchImpl: typeof globalThis.fetch): Promise<Metadata>;
package/dist/discovery.js CHANGED
@@ -1,57 +1,57 @@
1
1
  /**
2
- * Wo bei Aplons was liegtgefragt, nicht geraten.
2
+ * Where things live at Aplonsasked for, not guessed.
3
3
  *
4
- * Die Endpunkte ließen sich auch fest eintragen; genau das ist in diesem
5
- * Projekt schon schiefgegangen, als eine handgeschriebene Liste von Bereichen
6
- * neben der echten herlief und Anwendungen die falschen anforderten. Wer
7
- * fragt, bekommt immer die Antwort von heute.
4
+ * The endpoints could be hard-coded; that is exactly what went wrong in this
5
+ * project once already, when a hand-written list of scopes drifted away from
6
+ * the real one and applications requested the wrong things. Whoever asks
7
+ * gets today's answer.
8
8
  */
9
- import { AplonsError, ausAntwort } from "./errors.js";
10
- const PFLICHT = [
9
+ import { AplonsError, fromResponse } from "./errors.js";
10
+ const REQUIRED = [
11
11
  "issuer",
12
12
  "authorization_endpoint",
13
13
  "token_endpoint",
14
14
  "userinfo_endpoint",
15
15
  "jwks_uri",
16
16
  ];
17
- export async function holeMetadaten(issuer, fetchImpl) {
17
+ export async function fetchMetadata(issuer, fetchImpl) {
18
18
  const url = `${issuer}/.well-known/openid-configuration`;
19
- let antwort;
19
+ let response;
20
20
  try {
21
- antwort = await fetchImpl(url, { headers: { accept: "application/json" } });
21
+ response = await fetchImpl(url, { headers: { accept: "application/json" } });
22
22
  }
23
23
  catch (cause) {
24
24
  throw new AplonsError({
25
25
  code: "unreachable",
26
26
  cause,
27
- message: `${issuer} ist nicht erreichbar. Stimmt die Adresse, und kommt dieser ` +
28
- "Server überhaupt ins Netz?",
27
+ message: `${issuer} is not reachable. Is the address right, and can this ` +
28
+ "server reach the network at all?",
29
29
  });
30
30
  }
31
- if (!antwort.ok)
32
- throw await ausAntwort(antwort, `${url} zu laden`);
33
- const metadaten = (await antwort.json());
34
- for (const feld of PFLICHT) {
35
- if (!metadaten[feld]) {
31
+ if (!response.ok)
32
+ throw await fromResponse(response, `Loading ${url}`);
33
+ const metadata = (await response.json());
34
+ for (const field of REQUIRED) {
35
+ if (!metadata[field]) {
36
36
  throw new AplonsError({
37
37
  code: "invalid_metadata",
38
- message: `${url} enthält kein ${feld}. Ist das wirklich ein Aplons-Server?`,
38
+ message: `${url} contains no ${field}. Is this really an Aplons server?`,
39
39
  });
40
40
  }
41
41
  }
42
42
  /*
43
- Der Aussteller muss zu der Adresse passen, unter der wir gefragt haben.
43
+ The issuer has to match the address we asked at.
44
44
 
45
- Ohne diese Prüfung könnte ein untergeschobener Discovery-URL auf fremde
46
- Endpunkte zeigen, und die Anwendung schickte ihre Anmeldungen dorthin.
47
- RFC 8414 verlangt die Prüfung aus genau diesem Grund.
45
+ Without this check a planted discovery URL could point at foreign
46
+ endpoints, and the application would send its logins there. RFC 8414
47
+ requires the check for exactly this reason.
48
48
  */
49
- if (metadaten.issuer.replace(/\/+$/, "") !== issuer) {
49
+ if (metadata.issuer.replace(/\/+$/, "") !== issuer) {
50
50
  throw new AplonsError({
51
51
  code: "issuer_mismatch",
52
- message: `Unter ${issuer} meldet sich ein Server, der sich „${metadaten.issuer}" ` +
53
- "nennt. Das darf nicht sein die Anmeldung wird nicht fortgesetzt.",
52
+ message: `The server at ${issuer} calls itself "${metadata.issuer}". That must ` +
53
+ "not happenthe login will not continue.",
54
54
  });
55
55
  }
56
- return metadaten;
56
+ return metadata;
57
57
  }
package/dist/errors.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Ein Fehler, der sagt, was zu tun ist.
2
+ * An error that says what to do about it.
3
3
  *
4
- * OAuth antwortet mit Kennungen wie `invalid_grant` — richtig für eine
5
- * Maschine, nutzlos für den, der um drei Uhr nachts in ein Log sieht. Jeder
6
- * Fehler hier trägt deshalb beides: die Kennung für den Code und einen Satz
7
- * für den Menschen.
4
+ * OAuth answers with identifiers like `invalid_grant` — right for a machine,
5
+ * useless for the person reading a log at three in the morning. Every error
6
+ * here carries both: the identifier for the code and a sentence for the
7
+ * human.
8
8
  */
9
9
  export declare class AplonsError extends Error {
10
- /** Die OAuth-Kennung, etwa `invalid_grant`. */
10
+ /** The OAuth identifier, e.g. `invalid_grant`. */
11
11
  readonly code: string;
12
- /** Der HTTP-Status, wenn der Fehler von einer Antwort kam. */
12
+ /** The HTTP status, when the error came from a response. */
13
13
  readonly status?: number;
14
- /** Was der Server dazu geschrieben hat. */
14
+ /** What the server wrote about it. */
15
15
  readonly description?: string;
16
16
  constructor(options: {
17
17
  code: string;
@@ -21,7 +21,7 @@ export declare class AplonsError extends Error {
21
21
  cause?: unknown;
22
22
  });
23
23
  }
24
- /** Welcher Ablauf gerade fehlgeschlagen ist. */
25
- export type Ablauf = "code" | "refresh" | "allgemein";
26
- /** Aus einer fehlgeschlagenen Antwort einen brauchbaren Fehler machen. */
27
- export declare function ausAntwort(antwort: Response, wobei: string, ablauf?: Ablauf): Promise<AplonsError>;
24
+ /** Which flow just failed. */
25
+ export type Flow = "code" | "refresh" | "generic";
26
+ /** Turn a failed response into something usable. */
27
+ export declare function fromResponse(response: Response, what: string, flow?: Flow): Promise<AplonsError>;