@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/README.md
CHANGED
|
@@ -17,14 +17,14 @@ Eine Datei, vier Adressen:
|
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
19
|
// app/api/auth/[...aplons]/route.ts
|
|
20
|
-
import {
|
|
20
|
+
import { createHandler } from "@aplons/auth/next";
|
|
21
21
|
|
|
22
|
-
export const { GET, POST } =
|
|
22
|
+
export const { GET, POST } = createHandler({
|
|
23
23
|
issuer: process.env.APLONS_ISSUER!, // https://auth.aplons.com
|
|
24
24
|
clientId: process.env.APLONS_CLIENT_ID!,
|
|
25
25
|
clientSecret: process.env.APLONS_CLIENT_SECRET,
|
|
26
26
|
redirectUri: process.env.APLONS_REDIRECT_URI!,
|
|
27
|
-
|
|
27
|
+
afterLogin: "/",
|
|
28
28
|
});
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -32,28 +32,29 @@ Das ergibt:
|
|
|
32
32
|
|
|
33
33
|
| Adresse | wozu |
|
|
34
34
|
|---|---|
|
|
35
|
-
| `/api/auth/login` | Anmeldung starten. `?
|
|
35
|
+
| `/api/auth/login` | Anmeldung starten. `?next=/pfad` merkt sich, wohin danach. |
|
|
36
36
|
| `/api/auth/callback` | Rückkehr von Aplons. Hier ist nichts zu tun. |
|
|
37
37
|
| `/api/auth/logout` | Abmelden — hier **und** bei Aplons. |
|
|
38
38
|
| `/api/auth/me` | Wer angemeldet ist; erneuert das Token still, wenn nötig. |
|
|
39
|
+
| `/api/auth/branding` | Logo, Farben und Eckenradius des Mandanten. |
|
|
39
40
|
|
|
40
41
|
Ein Anmeldeknopf ist dann ein Link:
|
|
41
42
|
|
|
42
43
|
```tsx
|
|
43
|
-
<a href="/api/auth/login?
|
|
44
|
+
<a href="/api/auth/login?next=/rechnungen">Anmelden</a>
|
|
44
45
|
```
|
|
45
46
|
|
|
46
47
|
Die Sitzung in einer eigenen Route lesen:
|
|
47
48
|
|
|
48
49
|
```ts
|
|
49
|
-
const {
|
|
50
|
+
const { getSession, auth } = createHandler({ /* … */ });
|
|
50
51
|
|
|
51
52
|
export async function GET(request: Request) {
|
|
52
|
-
const
|
|
53
|
-
if (!
|
|
53
|
+
const session = await getSession(request);
|
|
54
|
+
if (!session) return new Response("nicht angemeldet", { status: 401 });
|
|
54
55
|
|
|
55
|
-
const
|
|
56
|
-
return Response.json(
|
|
56
|
+
const user = await auth.userInfo(session.accessToken);
|
|
57
|
+
return Response.json(user);
|
|
57
58
|
}
|
|
58
59
|
```
|
|
59
60
|
|
|
@@ -70,19 +71,19 @@ const auth = new AplonsAuth({
|
|
|
70
71
|
});
|
|
71
72
|
|
|
72
73
|
// 1. Hinschicken
|
|
73
|
-
const { url, verifier, state, nonce } = await auth.
|
|
74
|
+
const { url, verifier, state, nonce } = await auth.startLogin();
|
|
74
75
|
// verifier, state und nonce bis zur Rückkehr aufbewahren
|
|
75
76
|
|
|
76
77
|
// 2. Zurückkommen
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
const session = await auth.completeLogin({ url: requestUrl, verifier, state, nonce });
|
|
79
|
+
session.accessToken;
|
|
80
|
+
session.claims?.email;
|
|
80
81
|
|
|
81
82
|
// 3. Erneuern
|
|
82
|
-
const
|
|
83
|
+
const fresh = await auth.refresh(session.refreshToken!);
|
|
83
84
|
|
|
84
85
|
// 4. Abmelden
|
|
85
|
-
const
|
|
86
|
+
const logout = await auth.logoutUrl({ returnTo: "https://app.example.de/" });
|
|
86
87
|
```
|
|
87
88
|
|
|
88
89
|
**`verifier`, `state` und `nonce` gehören in ein kurzlebiges, `httpOnly`
|
|
@@ -90,10 +91,53 @@ gesetztes Cookie — nicht in den `localStorage`.** Was dort liegt, liest jedes
|
|
|
90
91
|
Skript auf der Seite, und mit dem Verifier lässt sich ein abgefangener
|
|
91
92
|
Anmeldecode einlösen. Der Next-Teil oben macht das schon richtig.
|
|
92
93
|
|
|
94
|
+
## Aussehen wie die Firma
|
|
95
|
+
|
|
96
|
+
Eine angebundene Anwendung meldet Leute nicht nur an, sie zeigt ihnen danach
|
|
97
|
+
etwas. Ein Intranet-Hub, ein internes Werkzeug, ein Kundenportal: alle sollen
|
|
98
|
+
aussehen wie die Firma, für die sie gebaut sind. Das Logo dafür ein zweites
|
|
99
|
+
Mal einzupflegen ist die Art Arbeit, die genau einmal gemacht und beim
|
|
100
|
+
nächsten Rebranding vergessen wird — dann trägt die Anmeldung das neue Logo
|
|
101
|
+
und der Hub daneben das alte.
|
|
102
|
+
|
|
103
|
+
Also von dort, wo es ohnehin gepflegt wird:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const branding = await auth.branding(session.accessToken);
|
|
107
|
+
|
|
108
|
+
branding.logoUrl; // vollständige Adresse, direkt für ein <img>
|
|
109
|
+
branding.colors.primary; // "#4882fe"
|
|
110
|
+
branding.radius; // 8
|
|
111
|
+
branding.tenant.name; // "Muster GmbH"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Als CSS-Variablen, wenn die ganze Oberfläche sie tragen soll:
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { brandingCss } from "@aplons/auth";
|
|
118
|
+
|
|
119
|
+
<style>{brandingCss(branding)}</style>
|
|
120
|
+
<button style={{ background: "var(--aplons-primary)", borderRadius: "var(--aplons-radius)" }}>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Mit Next.js geht es auch ohne eigenen Aufruf: `/api/auth/branding` liefert
|
|
124
|
+
dasselbe für die angemeldete Sitzung, und `getBranding(request)` holt es in
|
|
125
|
+
einer eigenen Route.
|
|
126
|
+
|
|
127
|
+
Zwei Dinge müssen dafür stimmen, und die Fehlermeldung sagt welches:
|
|
128
|
+
|
|
129
|
+
| `code` | heißt |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `insufficient_scope` | Dein Token trägt den Scope `branding` nicht — trag ihn bei der Anwendung ein und fordere ihn an (`scope: ["openid", "profile", "email", "branding"]`). |
|
|
132
|
+
| `branding_not_shared` | Der Mandant gibt sein Erscheinungsbild nicht heraus. Das steht in **seinen** Richtlinien und ist ab Werk aus; nur er kann das ändern. |
|
|
133
|
+
|
|
134
|
+
**Speichere das Ergebnis zwischen.** Ein Logo wechselt selten, und eine Seite,
|
|
135
|
+
die es bei jedem Aufruf neu holt, wartet dafür jedes Mal auf das Netz.
|
|
136
|
+
|
|
93
137
|
## Eine API dahinter absichern
|
|
94
138
|
|
|
95
139
|
```ts
|
|
96
|
-
const claims = await auth.
|
|
140
|
+
const claims = await auth.verifyAccessToken(token);
|
|
97
141
|
claims.sub; // das Konto
|
|
98
142
|
claims.tid; // der Mandant
|
|
99
143
|
claims.rls; // Rollen im Mandanten
|
|
@@ -123,9 +167,9 @@ Meldung für den Menschen:
|
|
|
123
167
|
import { AplonsError } from "@aplons/auth";
|
|
124
168
|
|
|
125
169
|
try {
|
|
126
|
-
await auth.
|
|
127
|
-
} catch (
|
|
128
|
-
if (
|
|
170
|
+
await auth.completeLogin({ /* … */ });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (error instanceof AplonsError && error.code === "invalid_grant") {
|
|
129
173
|
// meistens: die Rückkehr wurde zweimal ausgeführt
|
|
130
174
|
}
|
|
131
175
|
}
|
|
@@ -142,9 +186,21 @@ Häufig:
|
|
|
142
186
|
|
|
143
187
|
## Bereiche
|
|
144
188
|
|
|
145
|
-
`openid`, `profile`, `email`, `phone`, `roles`, `app_profile
|
|
146
|
-
sind die ersten drei. `roles` liefert die
|
|
147
|
-
Anwendung**, `app_profile` deren eigene Felder —
|
|
189
|
+
`openid`, `profile`, `email`, `phone`, `roles`, `app_profile`, `branding`,
|
|
190
|
+
`offline_access`. Voreinstellung sind die ersten drei. `roles` liefert die
|
|
191
|
+
Rollen des Kontos **in dieser Anwendung**, `app_profile` deren eigene Felder —
|
|
192
|
+
beide über `auth.userInfo()`. `branding` das Erscheinungsbild des Mandanten,
|
|
193
|
+
über `auth.branding()`.
|
|
194
|
+
|
|
195
|
+
`offline_access` darf jeder Client anfordern, auch ohne ihn eingetragen zu
|
|
196
|
+
haben, und bewirkt nichts: ein Refresh-Token kommt beim Tausch des
|
|
197
|
+
Anmeldecodes ohnehin immer zurück. Er steht hier, weil er der übliche Weg
|
|
198
|
+
ist, danach zu fragen, und manche Bibliotheken ihn von sich aus mitschicken.
|
|
199
|
+
|
|
200
|
+
Alle anderen Bereiche muss die Anwendung im Mandanten eingetragen haben.
|
|
201
|
+
Fehlt einer, kommt von `/oauth/authorize` ein `invalid_scope` zurück, dessen
|
|
202
|
+
Text zwischen „kennt der Server nicht" und „diese Anwendung hat ihn nicht
|
|
203
|
+
eingetragen" unterscheidet.
|
|
148
204
|
|
|
149
205
|
## Sitzungen ablegen
|
|
150
206
|
|
|
@@ -153,12 +209,16 @@ Speicher aus, hat aber zwei Grenzen: 4 KB, und ein Widerruf wirkt erst, wenn
|
|
|
153
209
|
das Zugriffstoken abläuft. Wer eine Datenbank hat, gibt seine eigene Ablage an:
|
|
154
210
|
|
|
155
211
|
```ts
|
|
156
|
-
|
|
212
|
+
createHandler({
|
|
157
213
|
/* … */
|
|
158
|
-
|
|
159
|
-
async
|
|
160
|
-
async
|
|
161
|
-
async
|
|
214
|
+
store: {
|
|
215
|
+
async read(id) { /* … */ },
|
|
216
|
+
async write(id, session) { /* … */ },
|
|
217
|
+
async delete(id) { /* … */ },
|
|
162
218
|
},
|
|
163
219
|
});
|
|
164
220
|
```
|
|
221
|
+
|
|
222
|
+
Alle drei werden gebraucht: `delete` beim Abmelden, `write` bei jeder stillen
|
|
223
|
+
Erneuerung — und zwar unter **derselben** Kennung, damit aus einer Sitzung
|
|
224
|
+
nicht alle zehn Minuten eine weitere Zeile wird.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Das Erscheinungsbild des Mandanten — Logo, Farben, Eckenradius.
|
|
3
|
+
*
|
|
4
|
+
* Eine angebundene Anwendung meldet Leute nicht nur an, sie zeigt ihnen
|
|
5
|
+
* danach etwas. Ein Intranet-Hub, ein internes Werkzeug, ein Kundenportal:
|
|
6
|
+
* alle sollen aussehen wie die Firma, für die sie gebaut sind. Das Logo dafür
|
|
7
|
+
* ein zweites Mal einzupflegen ist die Art von Arbeit, die genau einmal
|
|
8
|
+
* gemacht und danach beim Rebranding vergessen wird — dann trägt die
|
|
9
|
+
* Anmeldung das neue Logo und der Hub daneben das alte.
|
|
10
|
+
*
|
|
11
|
+
* Also von dort, wo es ohnehin gepflegt wird:
|
|
12
|
+
*
|
|
13
|
+
* const branding = await auth.branding(session.accessToken);
|
|
14
|
+
* branding.logoUrl;
|
|
15
|
+
* branding.colors.primary;
|
|
16
|
+
*
|
|
17
|
+
* Zwei Dinge müssen dafür stimmen, und die Fehlermeldung sagt welches:
|
|
18
|
+
* die Anwendung braucht den Bereich `branding`, und der Mandant muss das
|
|
19
|
+
* Weitergeben in seinen Richtlinien erlaubt haben. Letzteres ist ab Werk aus.
|
|
20
|
+
*/
|
|
21
|
+
/** Was `auth.branding()` zurückgibt. */
|
|
22
|
+
export type Branding = {
|
|
23
|
+
tenant: {
|
|
24
|
+
slug: string;
|
|
25
|
+
/** Anzeigename des Mandanten. */
|
|
26
|
+
name: string;
|
|
27
|
+
/** Der oberste Mandant darüber — für „bereitgestellt von". */
|
|
28
|
+
rootName: string;
|
|
29
|
+
};
|
|
30
|
+
logoUrl?: string;
|
|
31
|
+
/** Fassung für dunkle Oberflächen, falls hinterlegt. */
|
|
32
|
+
logoUrlDark?: string;
|
|
33
|
+
faviconUrl?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Gesetzt heißt: der Mandant legt hell oder dunkel fest. Fehlt der Wert,
|
|
36
|
+
* entscheidet die Anwendung — in aller Regel: was der Besucher eingestellt
|
|
37
|
+
* hat.
|
|
38
|
+
*/
|
|
39
|
+
colorScheme?: "light" | "dark";
|
|
40
|
+
colors: {
|
|
41
|
+
primary: string;
|
|
42
|
+
/** Was auf der Primärfarbe lesbar ist. */
|
|
43
|
+
primaryText: string;
|
|
44
|
+
background: string;
|
|
45
|
+
surface: string;
|
|
46
|
+
text: string;
|
|
47
|
+
mutedText: string;
|
|
48
|
+
border: string;
|
|
49
|
+
danger: string;
|
|
50
|
+
};
|
|
51
|
+
/** Eckenradius in Pixeln. */
|
|
52
|
+
radius: number;
|
|
53
|
+
logo: {
|
|
54
|
+
height: number;
|
|
55
|
+
maxWidth: number;
|
|
56
|
+
/** Steht der Name neben dem Logo? Fehlt der Wert: Sache der Anwendung. */
|
|
57
|
+
showText?: boolean;
|
|
58
|
+
/** Was dort steht, wenn nicht der Mandantenname. */
|
|
59
|
+
text?: string;
|
|
60
|
+
};
|
|
61
|
+
imprintUrl?: string;
|
|
62
|
+
privacyUrl?: string;
|
|
63
|
+
supportEmail?: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Die Farben und der Eckenradius als CSS-Variablen.
|
|
67
|
+
*
|
|
68
|
+
* Gedacht für ein `<style>` im Kopf der Seite oder für ein `style`-Attribut
|
|
69
|
+
* am äußersten Element:
|
|
70
|
+
*
|
|
71
|
+
* <style>{brandingCss(branding)}</style>
|
|
72
|
+
* …
|
|
73
|
+
* <button style={{ background: "var(--aplons-primary)" }}>
|
|
74
|
+
*
|
|
75
|
+
* Nur Farben und Radius: ein Logo ist ein Bild und gehört in ein `<img>`, wo
|
|
76
|
+
* es einen Alternativtext bekommt, statt als Hintergrund in einem Stylesheet
|
|
77
|
+
* zu verschwinden.
|
|
78
|
+
*/
|
|
79
|
+
export declare function brandingCss(branding: Branding, options?: {
|
|
80
|
+
/** Woran die Variablen hängen. Voreinstellung `:root`. */
|
|
81
|
+
selector?: string;
|
|
82
|
+
}): string;
|
package/dist/branding.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Das Erscheinungsbild des Mandanten — Logo, Farben, Eckenradius.
|
|
3
|
+
*
|
|
4
|
+
* Eine angebundene Anwendung meldet Leute nicht nur an, sie zeigt ihnen
|
|
5
|
+
* danach etwas. Ein Intranet-Hub, ein internes Werkzeug, ein Kundenportal:
|
|
6
|
+
* alle sollen aussehen wie die Firma, für die sie gebaut sind. Das Logo dafür
|
|
7
|
+
* ein zweites Mal einzupflegen ist die Art von Arbeit, die genau einmal
|
|
8
|
+
* gemacht und danach beim Rebranding vergessen wird — dann trägt die
|
|
9
|
+
* Anmeldung das neue Logo und der Hub daneben das alte.
|
|
10
|
+
*
|
|
11
|
+
* Also von dort, wo es ohnehin gepflegt wird:
|
|
12
|
+
*
|
|
13
|
+
* const branding = await auth.branding(session.accessToken);
|
|
14
|
+
* branding.logoUrl;
|
|
15
|
+
* branding.colors.primary;
|
|
16
|
+
*
|
|
17
|
+
* Zwei Dinge müssen dafür stimmen, und die Fehlermeldung sagt welches:
|
|
18
|
+
* die Anwendung braucht den Bereich `branding`, und der Mandant muss das
|
|
19
|
+
* Weitergeben in seinen Richtlinien erlaubt haben. Letzteres ist ab Werk aus.
|
|
20
|
+
*/
|
|
21
|
+
/** Die Namen, unter denen `brandingCss` die Werte ablegt. */
|
|
22
|
+
const VARIABLES = [
|
|
23
|
+
["primary", "primary"],
|
|
24
|
+
["primaryText", "primary-text"],
|
|
25
|
+
["background", "background"],
|
|
26
|
+
["surface", "surface"],
|
|
27
|
+
["text", "text"],
|
|
28
|
+
["mutedText", "muted-text"],
|
|
29
|
+
["border", "border"],
|
|
30
|
+
["danger", "danger"],
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Genau eine Hex-Farbe, sonst nichts.
|
|
34
|
+
*
|
|
35
|
+
* Der Server prüft das schon. Hier wird es trotzdem geprüft, weil das
|
|
36
|
+
* Ergebnis in ein `<style>` geschrieben wird: käme dort jemals ein anderer
|
|
37
|
+
* Text an — durch einen Zwischenspeicher, einen Vermittlungsserver, eine
|
|
38
|
+
* spätere Änderung am Server —, stünde er in einem Stylesheet, und ein
|
|
39
|
+
* Stylesheet lässt sich zum Auslesen von Eingaben missbrauchen. Eine Prüfung,
|
|
40
|
+
* die an zwei Stellen steht, kostet nichts; eine, die nur an einer steht,
|
|
41
|
+
* kostet genau dann etwas, wenn diese eine sich ändert.
|
|
42
|
+
*/
|
|
43
|
+
function hex(value) {
|
|
44
|
+
return typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)
|
|
45
|
+
? value.toLowerCase()
|
|
46
|
+
: null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Die Farben und der Eckenradius als CSS-Variablen.
|
|
50
|
+
*
|
|
51
|
+
* Gedacht für ein `<style>` im Kopf der Seite oder für ein `style`-Attribut
|
|
52
|
+
* am äußersten Element:
|
|
53
|
+
*
|
|
54
|
+
* <style>{brandingCss(branding)}</style>
|
|
55
|
+
* …
|
|
56
|
+
* <button style={{ background: "var(--aplons-primary)" }}>
|
|
57
|
+
*
|
|
58
|
+
* Nur Farben und Radius: ein Logo ist ein Bild und gehört in ein `<img>`, wo
|
|
59
|
+
* es einen Alternativtext bekommt, statt als Hintergrund in einem Stylesheet
|
|
60
|
+
* zu verschwinden.
|
|
61
|
+
*/
|
|
62
|
+
export function brandingCss(branding, options) {
|
|
63
|
+
const declarations = [];
|
|
64
|
+
for (const [key, name] of VARIABLES) {
|
|
65
|
+
const value = hex(branding.colors?.[key]);
|
|
66
|
+
if (value)
|
|
67
|
+
declarations.push(`--aplons-${name}: ${value};`);
|
|
68
|
+
}
|
|
69
|
+
const radius = Number(branding.radius);
|
|
70
|
+
if (Number.isFinite(radius)) {
|
|
71
|
+
declarations.push(`--aplons-radius: ${Math.min(Math.max(Math.round(radius), 0), 64)}px;`);
|
|
72
|
+
}
|
|
73
|
+
if (declarations.length === 0)
|
|
74
|
+
return "";
|
|
75
|
+
// Der Selektor kommt vom Aufrufer, nicht vom Server — trotzdem ohne
|
|
76
|
+
// geschweifte Klammern, damit ein Tippfehler nicht mitten im Block einen
|
|
77
|
+
// neuen aufmacht.
|
|
78
|
+
const selector = (options?.selector ?? ":root").replace(/[{}]/g, "");
|
|
79
|
+
return `${selector} { ${declarations.join(" ")} }`;
|
|
80
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The connection to Aplons.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
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 { type
|
|
13
|
-
import type {
|
|
11
|
+
import { type Metadata } from "./discovery.js";
|
|
12
|
+
import type { Branding } from "./branding.js";
|
|
13
|
+
import type { AccessTokenClaims, AplonsOptions, AuthorizationRequest, Session, UserInfo } from "./types.js";
|
|
14
14
|
export declare class AplonsAuth {
|
|
15
15
|
#private;
|
|
16
16
|
readonly issuer: string;
|
|
@@ -19,75 +19,98 @@ export declare class AplonsAuth {
|
|
|
19
19
|
readonly scope: string[];
|
|
20
20
|
constructor(options: AplonsOptions);
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* The endpoints, fetched once and then kept.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* Cached as a promise, not as a result: otherwise ten concurrent requests
|
|
25
|
+
* at startup fetch the same document ten times.
|
|
26
26
|
*/
|
|
27
|
-
|
|
27
|
+
metadata(): Promise<Metadata>;
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* Step 1: where to send the browser.
|
|
30
30
|
*
|
|
31
|
-
* `verifier`
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* `verifier`, `state` and `nonce` have to survive until the callback — in a
|
|
32
|
+
* short-lived, `httpOnly` cookie, not in localStorage: whatever sits there
|
|
33
|
+
* is readable by every script on the page.
|
|
34
34
|
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
35
|
+
startLogin(options?: {
|
|
36
|
+
/** Overrides the scopes from the constructor. */
|
|
37
37
|
scope?: string[];
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
/**
|
|
38
|
+
/** Passed through unchanged — e.g. the page that was being opened. */
|
|
39
|
+
extraParams?: Record<string, string>;
|
|
40
|
+
/** Forces the login screen even when a session already exists. */
|
|
41
|
+
forceLogin?: boolean;
|
|
42
|
+
/** Puts the login screen on this tenant. */
|
|
43
43
|
tenant?: string;
|
|
44
|
-
}): Promise<
|
|
44
|
+
}): Promise<AuthorizationRequest>;
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Step 2: the callback.
|
|
47
47
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* Takes the full URL the browser came back with, plus the values from
|
|
49
|
+
* step 1.
|
|
50
50
|
*/
|
|
51
|
-
|
|
52
|
-
/**
|
|
51
|
+
completeLogin(options: {
|
|
52
|
+
/** The incoming URL, complete. */
|
|
53
53
|
url: string | URL;
|
|
54
54
|
verifier: string;
|
|
55
|
-
/**
|
|
55
|
+
/** The state from step 1 — checked against the one in the URL. */
|
|
56
56
|
state: string;
|
|
57
|
-
/**
|
|
57
|
+
/** The nonce from step 1, when an ID token is expected. */
|
|
58
58
|
nonce?: string;
|
|
59
|
-
}): Promise<
|
|
60
|
-
/**
|
|
61
|
-
|
|
59
|
+
}): Promise<Session>;
|
|
60
|
+
/** Step 3: trade an expired access token for a fresh one. */
|
|
61
|
+
refresh(refreshToken: string): Promise<Session>;
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
63
|
+
* Step 4: log out — at Aplons, not just here.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
65
|
+
* Clearing your own session is not enough: the session at Aplons would
|
|
66
|
+
* survive, and the next login would sail through without a password. On a
|
|
67
|
+
* shared machine that is the difference between signed out and apparently
|
|
68
|
+
* signed out.
|
|
69
69
|
*/
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
|
|
73
|
-
/**
|
|
70
|
+
logoutUrl(options?: {
|
|
71
|
+
/** Where Aplons sends the browser after logging out. Must be registered. */
|
|
72
|
+
returnTo?: string;
|
|
73
|
+
/** The session's ID token; it tells Aplons who is being logged out. */
|
|
74
74
|
idToken?: string;
|
|
75
75
|
}): Promise<string>;
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
77
|
+
* Verify an access token — for whoever has their own API behind it.
|
|
78
78
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
79
|
+
* Checked against the public keys of Aplons, without a round trip on every
|
|
80
|
+
* call: signature, issuer and expiry. That is the difference between "the
|
|
81
|
+
* token looks real" and "the token is real".
|
|
82
82
|
*/
|
|
83
|
-
|
|
84
|
-
/** Die Angaben zum angemeldeten Konto, so weit die Bereiche es hergeben. */
|
|
85
|
-
profil(accessToken: string): Promise<Profil>;
|
|
83
|
+
verifyAccessToken(token: string): Promise<AccessTokenClaims>;
|
|
86
84
|
/**
|
|
87
|
-
*
|
|
85
|
+
* Das Erscheinungsbild des Mandanten — Logo, Farben, Eckenradius.
|
|
88
86
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
87
|
+
* Ein Aufruf, damit eine eingebundene Oberfläche aussieht wie die Firma,
|
|
88
|
+
* für die sie gebaut ist, statt deren Logo ein zweites Mal zu pflegen:
|
|
89
|
+
*
|
|
90
|
+
* const branding = await auth.branding(session.accessToken);
|
|
91
|
+
*
|
|
92
|
+
* Welcher Mandant, steht im Token — nicht im Aufruf. Ein Konto bekommt
|
|
93
|
+
* damit immer genau das Erscheinungsbild, das es auf der Anmeldeseite auch
|
|
94
|
+
* gesehen hat.
|
|
95
|
+
*
|
|
96
|
+
* Es lohnt sich, das Ergebnis zwischenzuspeichern: ein Logo wechselt selten,
|
|
97
|
+
* und eine Seite, die es bei jedem Aufruf neu holt, wartet dafür jedes Mal
|
|
98
|
+
* auf eine Antwort aus dem Netz.
|
|
99
|
+
*
|
|
100
|
+
* Zwei Absagen sind möglich und meinen Verschiedenes: `insufficient_scope`
|
|
101
|
+
* heißt, das Token trägt den Bereich `branding` nicht — dann fehlt er in den
|
|
102
|
+
* `scope`s dieser Anwendung. `branding_not_shared` heißt, der Mandant gibt
|
|
103
|
+
* sein Erscheinungsbild nicht heraus; das steht in seinen Richtlinien und
|
|
104
|
+
* ist ab Werk aus.
|
|
105
|
+
*/
|
|
106
|
+
branding(accessToken: string): Promise<Branding>;
|
|
107
|
+
/** The details of the signed-in account, as far as the scopes allow. */
|
|
108
|
+
userInfo(accessToken: string): Promise<UserInfo>;
|
|
109
|
+
/**
|
|
110
|
+
* Revoke a refresh token.
|
|
111
|
+
*
|
|
112
|
+
* Part of logging out: a token still valid for thirty days does not become
|
|
113
|
+
* invalid because a cookie was deleted.
|
|
91
114
|
*/
|
|
92
|
-
|
|
115
|
+
revoke(refreshToken: string): Promise<void>;
|
|
93
116
|
}
|