@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/pkce.js CHANGED
@@ -1,75 +1,73 @@
1
1
  /**
2
- * PKCE — der Nachweis, dass der, der den Code einlöst, auch der ist, der ihn
3
- * angefordert hat.
2
+ * PKCE — proof that whoever redeems the code is the one who asked for it.
4
3
  *
5
- * Der Code kommt über einen Umleitungs-URL zurück und steht damit im Verlauf
6
- * des Browsers, im Log jedes Proxys dazwischen und im Referer der nächsten
7
- * Seite. Wer ihn dort aufliest, kann ihn ohne PKCE einlösen. Mit PKCE braucht
8
- * er zusätzlich den Verifier, und der hat den Browser nie verlassen.
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
- * Alles hier läuft über die eingebaute Web-Crypto — kein Paket, keine
11
- * Abhängigkeit. Verfügbar in Node ab 18, in jedem Browser und am Rand.
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
- /** Der Zufall, aus dem alles Weitere folgt. */
14
- function zufall(bytes) {
15
- const puffer = new Uint8Array(bytes);
16
- crypto.getRandomValues(puffer);
17
- return puffer;
12
+ /** The randomness everything else follows from. */
13
+ function randomBytes(count) {
14
+ const buffer = new Uint8Array(count);
15
+ crypto.getRandomValues(buffer);
16
+ return buffer;
18
17
  }
19
18
  /**
20
- * Base64 ohne die drei Zeichen, die in einem URL etwas anderes bedeuten.
19
+ * Base64 without the three characters that mean something else in a URL.
21
20
  *
22
- * `+` wird in einer Formularkodierung zum Leerzeichen, `/` trennt Pfade, und
23
- * `=` trennt Parameter von Werten. Ein Verifier mit diesen Zeichen kommt am
24
- * anderen Ende verändert an und passt dann nicht mehr zu seinem Challenge
25
- * ein Fehler, der sich als „invalid_grant" zeigt und nach allem aussieht
26
- * außer nach seiner Ursache.
21
+ * `+` becomes a space in form encoding, `/` separates path segments, and `=`
22
+ * separates a parameter from its value. A verifier containing them arrives
23
+ * altered at the other end and no longer matches its challenge a failure
24
+ * that surfaces as `invalid_grant` and looks like anything but its cause.
27
25
  */
28
- export function base64url(daten) {
29
- const bytes = daten instanceof Uint8Array ? daten : new Uint8Array(daten);
30
- let roh = "";
26
+ export function base64url(data) {
27
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
28
+ let raw = "";
31
29
  for (const byte of bytes)
32
- roh += String.fromCharCode(byte);
33
- return btoa(roh).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
30
+ raw += String.fromCharCode(byte);
31
+ return btoa(raw).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
32
  }
35
33
  /**
36
- * Ein Verifier: 32 Byte Zufall, base64url — 43 Zeichen.
34
+ * A verifier: 32 bytes of randomness, base64url — 43 characters.
37
35
  *
38
- * Das ist die Untergrenze aus RFC 7636 und zugleich genug: 256 Bit Zufall
39
- * lassen sich nicht raten. Die Obergrenze von 128 Zeichen brächte nichts
40
- * dazu.
36
+ * That is the lower bound from RFC 7636 and it is also enough: 256 bits of
37
+ * randomness cannot be guessed. Going up to the 128-character limit would
38
+ * add nothing.
41
39
  */
42
40
  export function createVerifier() {
43
- return base64url(zufall(32));
41
+ return base64url(randomBytes(32));
44
42
  }
45
- /** Was davon in den Anmelde-URL geht: der Hash, nie der Verifier selbst. */
43
+ /** What goes into the authorization URL: the hash, never the verifier. */
46
44
  export async function createChallenge(verifier) {
47
45
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
48
46
  return base64url(digest);
49
47
  }
50
48
  /**
51
- * Der Wert gegen fremde Anfragen (CSRF).
49
+ * The value that guards against cross-site request forgery.
52
50
  *
53
- * Ohne ihn könnte jemand einen Anmeldevorgang mit *seinem* Konto beginnen und
54
- * dem Opfer den Rückkehr-URL unterschieben; das Opfer wäre danach im fremden
55
- * Konto angemeldet und legte dort Daten ab.
51
+ * Without it someone could start a login with *their* account and hand the
52
+ * victim the finished redirect URL; the victim would end up signed into a
53
+ * stranger's account and file their data there.
56
54
  */
57
55
  export function createState() {
58
- return base64url(zufall(16));
56
+ return base64url(randomBytes(16));
59
57
  }
60
58
  /**
61
- * Zwei Zeichenketten vergleichen, ohne über die Dauer zu verraten, ab welcher
62
- * Stelle sie sich unterscheiden.
59
+ * Compare two strings without revealing, through timing, where they start
60
+ * to differ.
63
61
  *
64
- * Für den State ist das strenggenommen mehr, als nötig wäre aber die
65
- * Funktion steht auch für den, der sie auf etwas Empfindlicheres anwendet.
62
+ * For the state this is strictly more than necessarybut the function is
63
+ * also here for whoever applies it to something more sensitive.
66
64
  */
67
- export function gleich(a, b) {
65
+ export function timingSafeEqual(a, b) {
68
66
  if (a.length !== b.length)
69
67
  return false;
70
- let unterschied = 0;
68
+ let difference = 0;
71
69
  for (let i = 0; i < a.length; i += 1) {
72
- unterschied |= a.charCodeAt(i) ^ b.charCodeAt(i);
70
+ difference |= a.charCodeAt(i) ^ b.charCodeAt(i);
73
71
  }
74
- return unterschied === 0;
72
+ return difference === 0;
75
73
  }
package/dist/types.d.ts CHANGED
@@ -1,36 +1,35 @@
1
- /** Wie eine Anwendung sich gegenüber Aplons ausweist. */
1
+ /** How an application identifies itself to Aplons. */
2
2
  export type AplonsOptions = {
3
3
  /**
4
- * Wo Aplons steht, ohne Pfadetwa `https://auth.aplons.com`.
5
- * Alles Weitere holt das Paket von dort selbst (`/.well-known/…`).
4
+ * Where Aplons lives, without a path e.g. `https://auth.aplons.com`.
5
+ * Everything else the package fetches from there itself (`/.well-known/…`).
6
6
  */
7
7
  issuer: string;
8
8
  clientId: string;
9
9
  /**
10
- * Nur für Anwendungen, die ein Geheimnis behalten könnenalso einen
11
- * Server haben. Eine Anwendung, die im Browser oder auf einem Telefon
12
- * läuft, lässt es weg: was dort mitgeliefert wird, ist nicht geheim, und
13
- * ein „Geheimnis", das jeder auslesen kann, macht die Sache nicht sicherer,
14
- * sondern nur unübersichtlich.
10
+ * Only for applications that can keep a secretthat is, ones with a
11
+ * server. An application running in a browser or on a phone leaves it out:
12
+ * whatever ships there is not secret, and a "secret" anyone can read does
13
+ * not make things safer, only less clear.
15
14
  */
16
15
  clientSecret?: string;
17
- /** Wohin Aplons nach der Anmeldung zurückschickt. Muss hinterlegt sein. */
16
+ /** Where Aplons sends the browser back to. Must be registered. */
18
17
  redirectUri: string;
19
- /** Voreinstellung: openid, profile, email. */
18
+ /** Defaults to openid, profile, email. */
20
19
  scope?: string[];
21
- /** Eigene fetch-Implementierung, etwa zum Testen. */
20
+ /** Your own fetch implementation, e.g. for tests. */
22
21
  fetch?: typeof globalThis.fetch;
23
22
  };
24
- /** Was nach der Anmeldung vorliegt. */
25
- export type Sitzung = {
23
+ /** What you hold after a successful login. */
24
+ export type Session = {
26
25
  accessToken: string;
27
26
  refreshToken?: string;
28
- /** Zeitpunkt, nicht Dauer: eine Dauer ist ab dem Moment falsch, in dem
29
- * man sie ablegt. */
27
+ /** A point in time, not a duration: a duration is wrong the moment you
28
+ * store it. */
30
29
  accessTokenExpiresAt: Date;
31
30
  idToken?: string;
32
31
  scope: string[];
33
- /** Die geprüften Angaben aus dem ID-Token. */
32
+ /** The verified claims from the ID token. */
34
33
  claims?: IdTokenClaims;
35
34
  };
36
35
  export type IdTokenClaims = {
@@ -44,29 +43,29 @@ export type IdTokenClaims = {
44
43
  name?: string;
45
44
  given_name?: string;
46
45
  family_name?: string;
47
- /** Der Mandant, zu dem das Konto gehört. */
46
+ /** The tenant the account belongs to. */
48
47
  tid?: string;
49
- [weitere: string]: unknown;
48
+ [claim: string]: unknown;
50
49
  };
51
50
  export type AccessTokenClaims = {
52
51
  sub: string;
53
52
  iss: string;
54
53
  exp: number;
55
54
  iat: number;
56
- /** Der Mandant. */
55
+ /** The tenant. */
57
56
  tid?: string;
58
- /** Rollen im Mandanten. */
57
+ /** Roles within the tenant. */
59
58
  rls?: string[];
60
- /** Einzelne Berechtigungen. */
59
+ /** Individual permissions. */
61
60
  prm?: string[];
62
- /** Die Sitzung, aus der das Token stammt. */
61
+ /** The session the token came from. */
63
62
  sid?: string;
64
63
  email?: string;
65
64
  scope?: string;
66
- [weitere: string]: unknown;
65
+ [claim: string]: unknown;
67
66
  };
68
- /** Was UserInfo zurückgibtje nach Bereich mehr oder weniger. */
69
- export type Profil = {
67
+ /** What UserInfo returnsmore or less, depending on the scopes. */
68
+ export type UserInfo = {
70
69
  sub: string;
71
70
  email?: string;
72
71
  email_verified?: boolean;
@@ -74,17 +73,17 @@ export type Profil = {
74
73
  given_name?: string;
75
74
  family_name?: string;
76
75
  phone_number?: string;
77
- /** Rollen in *dieser* Anwendung, mit dem Bereich `roles`. */
76
+ /** Roles in *this* application, with the `roles` scope. */
78
77
  roles?: string[];
79
- /** Die eigenen Felder dieser Anwendung, mit dem Bereich `app_profile`. */
78
+ /** This application's own fields, with the `app_profile` scope. */
80
79
  app_profile?: Record<string, unknown>;
81
- [weitere: string]: unknown;
80
+ [claim: string]: unknown;
82
81
  };
83
- /** Das, was zwischen Start und Rückkehr aufbewahrt werden muss. */
84
- export type Anmeldevorgang = {
85
- /** Dorthin schicken. */
82
+ /** What has to survive between starting a login and completing it. */
83
+ export type AuthorizationRequest = {
84
+ /** Send the browser here. */
86
85
  url: string;
87
- /** Beides kurzlebig ablegenund beim Rückkehr-Aufruf wieder mitgeben. */
86
+ /** Keep all three briefly and hand them back to `completeLogin`. */
88
87
  verifier: string;
89
88
  state: string;
90
89
  nonce: string;
package/dist/verify.d.ts CHANGED
@@ -1,26 +1,26 @@
1
1
  /**
2
- * Ein Token prüfen, statt ihm zu glauben.
2
+ * Verify a token instead of believing it.
3
3
  *
4
- * Ein JWT ist lesbar, ohne dass jemand etwas prüft — `atob` auf den mittleren
5
- * Teil, fertig. Wer das für eine Prüfung hält, lässt jeden herein, der sich
6
- * ein Token selbst schreibt. Geprüft wird deshalb die Unterschrift gegen die
7
- * öffentlichen Schlüssel von Aplons, dazu Aussteller, Empfänger und Ablauf.
4
+ * A JWT is readable without anyone verifying anything — `atob` on the middle
5
+ * part and you are done. Whoever mistakes that for verification lets in
6
+ * everybody who writes themselves a token. So the signature is checked
7
+ * against the public keys of Aplons, along with issuer, audience and expiry.
8
8
  *
9
- * `jose` erledigt das Kryptografische. Es ist die einzige Abhängigkeit dieses
10
- * Pakets, und es ist dieselbe, die der Aplons-Server selbst benutzt.
9
+ * `jose` does the cryptography. It is this package's only dependency, and it
10
+ * is the same one the Aplons server itself uses.
11
11
  */
12
12
  import { createRemoteJWKSet } from "jose";
13
13
  import type { AccessTokenClaims, IdTokenClaims } from "./types.js";
14
- type Schluesselquelle = ReturnType<typeof createRemoteJWKSet>;
15
- export declare function schluesselFuer(jwksUri: string): Schluesselquelle;
16
- export declare function pruefeZugriffstoken(token: string, options: {
14
+ type KeySource = ReturnType<typeof createRemoteJWKSet>;
15
+ export declare function jwksFor(jwksUri: string): KeySource;
16
+ export declare function verifyAccessToken(token: string, options: {
17
17
  issuer: string;
18
- schluessel: Schluesselquelle;
18
+ keys: KeySource;
19
19
  }): Promise<AccessTokenClaims>;
20
- export declare function pruefeIdToken(token: string, options: {
20
+ export declare function verifyIdToken(token: string, options: {
21
21
  issuer: string;
22
22
  audience: string;
23
23
  nonce?: string;
24
- schluessel: Schluesselquelle;
24
+ keys: KeySource;
25
25
  }): Promise<IdTokenClaims>;
26
- export {};
26
+ export type { KeySource };
package/dist/verify.js CHANGED
@@ -1,94 +1,94 @@
1
1
  /**
2
- * Ein Token prüfen, statt ihm zu glauben.
2
+ * Verify a token instead of believing it.
3
3
  *
4
- * Ein JWT ist lesbar, ohne dass jemand etwas prüft — `atob` auf den mittleren
5
- * Teil, fertig. Wer das für eine Prüfung hält, lässt jeden herein, der sich
6
- * ein Token selbst schreibt. Geprüft wird deshalb die Unterschrift gegen die
7
- * öffentlichen Schlüssel von Aplons, dazu Aussteller, Empfänger und Ablauf.
4
+ * A JWT is readable without anyone verifying anything — `atob` on the middle
5
+ * part and you are done. Whoever mistakes that for verification lets in
6
+ * everybody who writes themselves a token. So the signature is checked
7
+ * against the public keys of Aplons, along with issuer, audience and expiry.
8
8
  *
9
- * `jose` erledigt das Kryptografische. Es ist die einzige Abhängigkeit dieses
10
- * Pakets, und es ist dieselbe, die der Aplons-Server selbst benutzt.
9
+ * `jose` does the cryptography. It is this package's only dependency, and it
10
+ * is the same one the Aplons server itself uses.
11
11
  */
12
12
  import { createRemoteJWKSet, jwtVerify } from "jose";
13
13
  import { AplonsError } from "./errors.js";
14
14
  /*
15
- Eine Schlüsselquelle je Adresse, nicht je Aufruf.
15
+ One key source per address, not per call.
16
16
 
17
- `createRemoteJWKSet` bringt einen eigenen Zwischenspeicher mit und holt bei
18
- einem unbekannten `kid` neu das ist genau das Verhalten, das einen
19
- Schlüsselwechsel überlebt. Für jeden Aufruf eine neue Quelle anzulegen
20
- hieße, den Zwischenspeicher wegzuwerfen und bei jeder Anfrage die
21
- Schlüssel erneut zu laden.
17
+ `createRemoteJWKSet` brings its own cache and refetches on an unknown `kid`
18
+ exactly the behaviour that survives a key rotation. Building a new source
19
+ for every call would throw that cache away and refetch the keys on every
20
+ single request.
22
21
  */
23
- const quellen = new Map();
24
- export function schluesselFuer(jwksUri) {
25
- let quelle = quellen.get(jwksUri);
26
- if (!quelle) {
27
- quelle = createRemoteJWKSet(new URL(jwksUri));
28
- quellen.set(jwksUri, quelle);
22
+ const keySources = new Map();
23
+ export function jwksFor(jwksUri) {
24
+ let source = keySources.get(jwksUri);
25
+ if (!source) {
26
+ source = createRemoteJWKSet(new URL(jwksUri));
27
+ keySources.set(jwksUri, source);
29
28
  }
30
- return quelle;
29
+ return source;
31
30
  }
32
- export async function pruefeZugriffstoken(token, options) {
31
+ export async function verifyAccessToken(token, options) {
33
32
  try {
34
- const { payload } = await jwtVerify(token, options.schluessel, {
33
+ const { payload } = await jwtVerify(token, options.keys, {
35
34
  issuer: options.issuer,
36
- // Bewusst ohne `audience`: ein Zugriffstoken ist an die Anwendung
37
- // gerichtet, die es benutzt, und die kann eine andere sein als die,
38
- // die es geholt hat. Wer das enger fassen will, prüft `aud` selbst.
35
+ // Deliberately without `audience`: an access token is addressed to the
36
+ // application that uses it, and that can be a different one from the
37
+ // application that obtained it. Anyone wanting it stricter checks
38
+ // `aud` themselves.
39
39
  });
40
40
  return payload;
41
41
  }
42
42
  catch (cause) {
43
- throw alsFehler(cause, "Das Zugriffstoken");
43
+ throw asError(cause, "The access token");
44
44
  }
45
45
  }
46
- export async function pruefeIdToken(token, options) {
46
+ export async function verifyIdToken(token, options) {
47
47
  let claims;
48
48
  try {
49
- const { payload } = await jwtVerify(token, options.schluessel, {
49
+ const { payload } = await jwtVerify(token, options.keys, {
50
50
  issuer: options.issuer,
51
- // Hier schon: ein ID-Token ist an genau diese Anwendung gerichtet.
52
- // Eines, das für eine andere ausgestellt wurde, darf nicht zählen.
51
+ // Here it does apply: an ID token is addressed to exactly this
52
+ // application. One issued for another must not count.
53
53
  audience: options.audience,
54
54
  });
55
55
  claims = payload;
56
56
  }
57
57
  catch (cause) {
58
- throw alsFehler(cause, "Das ID-Token");
58
+ throw asError(cause, "The ID token");
59
59
  }
60
60
  /*
61
- Der Nonce bindet das ID-Token an genau diesen Anmeldevorgang.
61
+ The nonce binds the ID token to this one login.
62
62
 
63
- Ohne ihn ließe sich ein früher abgefangenes, noch gültiges ID-Token in
64
- einem neuen Vorgang einreichen. Geprüft wird nur, wenn beim Start einer
65
- gesetzt wurde sonst gäbe es nichts zu vergleichen.
63
+ Without it, an intercepted ID token that is still valid could be replayed
64
+ in a new flow. Only checked when one was set at the start — otherwise
65
+ there would be nothing to compare against.
66
66
  */
67
67
  if (options.nonce && claims.nonce !== options.nonce) {
68
68
  throw new AplonsError({
69
69
  code: "nonce_mismatch",
70
- message: "Der nonce im ID-Token gehört nicht zu diesem Anmeldevorgang. " +
71
- "Die Anmeldung wird nicht fortgesetzt.",
70
+ message: "The nonce in the ID token does not belong to this login. " +
71
+ "The login will not continue.",
72
72
  });
73
73
  }
74
74
  return claims;
75
75
  }
76
- function alsFehler(cause, was) {
76
+ function asError(cause, what) {
77
77
  const code = cause && typeof cause === "object" && "code" in cause
78
78
  ? String(cause.code)
79
79
  : "invalid_token";
80
- const erklaerung = {
81
- ERR_JWT_EXPIRED: "ist abgelaufen. Erneuere es mit dem Refresh-Token.",
82
- ERR_JWT_CLAIM_VALIDATION_FAILED: "wurde für einen anderen Aussteller oder Empfänger ausgestellt.",
83
- ERR_JWS_SIGNATURE_VERIFICATION_FAILED: "trägt keine gültige Unterschrift von Aplons.",
84
- ERR_JOSE_NOT_SUPPORTED: "benutzt ein Verfahren, das hier nicht zugelassen istbei " +
85
- '`alg: "none"` ist das ein selbst geschriebenes Token ohne Unterschrift.',
86
- ERR_JWKS_NO_MATCHING_KEY: "ist mit einem Schlüssel unterschrieben, den Aplons nicht kennt. " +
87
- "Bei einem gerade gewechselten Schlüssel hilft ein zweiter Versuch.",
80
+ const explanations = {
81
+ ERR_JWT_EXPIRED: "has expired. Renew it with the refresh token.",
82
+ ERR_JWT_CLAIM_VALIDATION_FAILED: "was issued for a different issuer or audience.",
83
+ ERR_JWS_SIGNATURE_VERIFICATION_FAILED: "does not carry a valid signature from Aplons.",
84
+ ERR_JOSE_NOT_SUPPORTED: 'uses an algorithm that is not allowed herewith `alg: "none"` that ' +
85
+ "is a self-written token with no signature at all.",
86
+ ERR_JWKS_NO_MATCHING_KEY: "is signed with a key Aplons does not know. If a key was just rotated, " +
87
+ "a second attempt helps.",
88
88
  };
89
89
  return new AplonsError({
90
90
  code,
91
91
  cause,
92
- message: `${was} ${erklaerung[code] ?? "ist nicht gültig."}`,
92
+ message: `${what} ${explanations[code] ?? "is not valid."}`,
93
93
  });
94
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aplons/auth",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Anmeldung über Aplons in eigenen Anwendungen — OAuth 2.1 mit PKCE, ohne den Ablauf selbst zu schreiben.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -23,9 +23,8 @@
23
23
  "default": "./dist/next.js"
24
24
  }
25
25
  },
26
- "scripts": {
27
- "build": "rm -rf dist && tsc -p tsconfig.json",
28
- "prepublishOnly": "pnpm build"
26
+ "publishConfig": {
27
+ "access": "public"
29
28
  },
30
29
  "dependencies": {
31
30
  "jose": "^6.1.0"
@@ -39,5 +38,8 @@
39
38
  "pkce",
40
39
  "authentication",
41
40
  "aplons"
42
- ]
43
- }
41
+ ],
42
+ "scripts": {
43
+ "build": "rm -rf dist && tsc -p tsconfig.json"
44
+ }
45
+ }