@multiplatform.one/keycloak 6.7.0 → 7.0.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.
Files changed (79) hide show
  1. package/dist/cjs/index.cjs +129 -55
  2. package/dist/cjs/index.native.cjs +239 -130
  3. package/dist/esm/index.js +131 -57
  4. package/dist/esm/index.native.js +239 -130
  5. package/package.json +15 -10
  6. package/src/betterAuth/expoAuthFlow.native.spec.ts +307 -0
  7. package/src/betterAuth/expoAuthFlow.native.ts +189 -55
  8. package/src/frappeToken.native.spec.ts +104 -0
  9. package/src/frappeToken.native.ts +46 -2
  10. package/src/frappeToken.spec.ts +141 -0
  11. package/src/frappeToken.ts +106 -14
  12. package/src/index.ts +1 -1
  13. package/src/keycloak/index.ts +24 -7
  14. package/src/keycloak/index.webext.ts +16 -0
  15. package/src/oauth/authorizationUrl.spec.ts +83 -0
  16. package/src/oauth/authorizationUrl.ts +74 -0
  17. package/src/oauth/callback.spec.ts +72 -0
  18. package/src/oauth/callback.ts +46 -0
  19. package/src/oauth/callbackPage.spec.ts +61 -0
  20. package/src/oauth/callbackPage.ts +57 -0
  21. package/src/oauth/endpoints.spec.ts +193 -0
  22. package/src/oauth/endpoints.ts +135 -0
  23. package/src/oauth/errors.ts +18 -0
  24. package/src/oauth/form.spec.ts +35 -0
  25. package/src/oauth/form.ts +30 -0
  26. package/src/oauth/idToken.spec.ts +191 -0
  27. package/src/oauth/idToken.ts +153 -0
  28. package/src/oauth/index.ts +10 -0
  29. package/src/oauth/jwt.ts +90 -0
  30. package/src/oauth/loopbackFlow.spec.ts +226 -0
  31. package/src/oauth/loopbackFlow.ts +114 -0
  32. package/src/oauth/pkce.spec.ts +60 -0
  33. package/src/oauth/pkce.ts +80 -0
  34. package/src/oauth/tokenExchange.spec.ts +331 -0
  35. package/src/oauth/tokenExchange.ts +232 -0
  36. package/src/one.ts +30 -3
  37. package/src/provider/AfterAuth.tsx +7 -0
  38. package/src/provider/authProvider/index.native.tsx +37 -4
  39. package/src/session/index.ts +2 -2
  40. package/src/state.ts +5 -2
  41. package/types/betterAuth/expoAuthFlow.native.d.ts +50 -3
  42. package/types/betterAuth/expoAuthFlow.native.d.ts.map +1 -1
  43. package/types/frappeToken.d.ts +14 -1
  44. package/types/frappeToken.d.ts.map +1 -1
  45. package/types/frappeToken.native.d.ts +16 -2
  46. package/types/frappeToken.native.d.ts.map +1 -1
  47. package/types/index.d.ts +1 -1
  48. package/types/index.d.ts.map +1 -1
  49. package/types/keycloak/index.d.ts.map +1 -1
  50. package/types/keycloak/index.webext.d.ts.map +1 -1
  51. package/types/oauth/authorizationUrl.d.ts +32 -0
  52. package/types/oauth/authorizationUrl.d.ts.map +1 -0
  53. package/types/oauth/callback.d.ts +26 -0
  54. package/types/oauth/callback.d.ts.map +1 -0
  55. package/types/oauth/callbackPage.d.ts +34 -0
  56. package/types/oauth/callbackPage.d.ts.map +1 -0
  57. package/types/oauth/endpoints.d.ts +68 -0
  58. package/types/oauth/endpoints.d.ts.map +1 -0
  59. package/types/oauth/errors.d.ts +17 -0
  60. package/types/oauth/errors.d.ts.map +1 -0
  61. package/types/oauth/form.d.ts +16 -0
  62. package/types/oauth/form.d.ts.map +1 -0
  63. package/types/oauth/idToken.d.ts +83 -0
  64. package/types/oauth/idToken.d.ts.map +1 -0
  65. package/types/oauth/index.d.ts +11 -0
  66. package/types/oauth/index.d.ts.map +1 -0
  67. package/types/oauth/jwt.d.ts +31 -0
  68. package/types/oauth/jwt.d.ts.map +1 -0
  69. package/types/oauth/loopbackFlow.d.ts +64 -0
  70. package/types/oauth/loopbackFlow.d.ts.map +1 -0
  71. package/types/oauth/pkce.d.ts +49 -0
  72. package/types/oauth/pkce.d.ts.map +1 -0
  73. package/types/oauth/tokenExchange.d.ts +89 -0
  74. package/types/oauth/tokenExchange.d.ts.map +1 -0
  75. package/types/one.d.ts +7 -0
  76. package/types/one.d.ts.map +1 -1
  77. package/types/provider/AfterAuth.d.ts.map +1 -1
  78. package/types/provider/authProvider/index.native.d.ts.map +1 -1
  79. package/types/state.d.ts.map +1 -1
@@ -12,9 +12,14 @@
12
12
  */
13
13
 
14
14
  import { useEffect, useMemo, useState } from "react";
15
+ import { AppState, type AppStateStatus } from "react-native";
15
16
  import * as Linking from "expo-linking";
16
17
  import { Loading } from "../../Loading";
17
- import { createExpoAuthFlow } from "../../betterAuth/expoAuthFlow.native";
18
+ import {
19
+ createExpoAuthFlow,
20
+ rewriteLoopbackHostForNative,
21
+ } from "../../betterAuth/expoAuthFlow.native";
22
+ import { clearKeycloakBearerToken } from "../../frappeToken.native";
18
23
  import { KeycloakConfigContext } from "../../keycloak/config";
19
24
  import { Keycloak } from "../../keycloak/index";
20
25
  import type { KeycloakLoginOptions, KeycloakLogoutOptions } from "../../keycloak/base";
@@ -35,8 +40,12 @@ export function AuthProvider({
35
40
  // Resolve the Better Auth server base URL from keycloak config.
36
41
  // On native, the keycloakConfig.url is the Keycloak server — the Better Auth
37
42
  // server is the app backend (CreateApp wires BETTER_AUTH_BASE_URL/BASE_URL).
43
+ // Loopback hosts are rewritten for the device (127.0.0.1 / 10.0.2.2): see
44
+ // rewriteLoopbackHostForNative.
38
45
  const serverBaseUrl = useMemo(() => {
39
- return keycloakConfig.betterAuthBaseUrl || "http://localhost:8000";
46
+ return rewriteLoopbackHostForNative(
47
+ keycloakConfig.betterAuthBaseUrl || "http://localhost:8000",
48
+ );
40
49
  }, [keycloakConfig]);
41
50
 
42
51
  const scheme = useMemo(() => keycloakConfig.expoScheme, [keycloakConfig]);
@@ -72,11 +81,24 @@ export function AuthProvider({
72
81
  // can only receive exp:// redirects (Linking.createURL derives
73
82
  // exp://<host>/--/ there; the bare app scheme in dev builds).
74
83
  await flow.signIn("keycloak", options.redirectUri || Linking.createURL("/"));
84
+ // Drop any signed-out cooldown/stale token so the first Frappe
85
+ // request after sign-in fetches a fresh Bearer token.
86
+ clearKeycloakBearerToken();
75
87
  await refresh();
76
88
  return undefined;
77
89
  },
78
- async (_options: KeycloakLogoutOptions) => {
79
- await flow.signOut();
90
+ async (options: KeycloakLogoutOptions) => {
91
+ // Single sign-out: ends the Better Auth session AND rides the
92
+ // Keycloak end-session endpoint through the system browser (see
93
+ // expoAuthFlow.signOut). Only custom-scheme URLs make sense as the
94
+ // post-logout return on native; web-style paths fall back to the
95
+ // app's own runtime URL.
96
+ const redirectUri =
97
+ options.redirectUri && options.redirectUri.includes("://")
98
+ ? options.redirectUri
99
+ : Linking.createURL("/");
100
+ await flow.signOut(redirectUri);
101
+ clearKeycloakBearerToken();
80
102
  await refresh();
81
103
  return undefined;
82
104
  },
@@ -105,8 +127,19 @@ export function AuthProvider({
105
127
  refresh().finally(() => {
106
128
  if (alive) setIsLoading(false);
107
129
  });
130
+
131
+ // Re-probe when the app returns to the foreground: covers a completed
132
+ // ASWebAuthenticationSession whose Linking event arrived after dismiss,
133
+ // and a user who abandoned the end-session page (local jar already
134
+ // empty; this just refreshes the header).
135
+ const onAppState = (next: AppStateStatus) => {
136
+ if (next === "active") refresh().catch(() => undefined);
137
+ };
138
+ const appSub = AppState.addEventListener("change", onAppState);
139
+
108
140
  return () => {
109
141
  alive = false;
142
+ appSub.remove();
110
143
  };
111
144
  }, [disabled, flow, keycloakConfig]);
112
145
 
@@ -6,7 +6,7 @@
6
6
  * Replaces the previous next-auth/react useSession.
7
7
  */
8
8
 
9
- import { isBrowser, isTauri } from "@multiplatform.one/platform";
9
+ import { isBrowser } from "@multiplatform.one/platform";
10
10
  import { getAuthClient } from "../betterAuth/client";
11
11
 
12
12
  export interface Session {
@@ -37,7 +37,7 @@ let useSession = <R extends boolean = false>(
37
37
  return { session: null, status: "unauthenticated" };
38
38
  };
39
39
 
40
- if (isBrowser && !isTauri) {
40
+ if (isBrowser) {
41
41
  useSession = <R extends boolean = false>(
42
42
  _options?: Record<string, unknown>,
43
43
  ): SessionContextValue<R> => {
package/src/state.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { createStore, useStore } from "@multiplatform.one/store";
2
- import { isBrowser, isIframe, isServer, isTauri } from "@multiplatform.one/platform";
2
+ import { isBrowser, isIframe, isServer } from "@multiplatform.one/platform";
3
3
 
4
- export const persist = isIframe || isTauri || (!isBrowser && !isServer);
4
+ // Browsers keep the session in a Better Auth cookie, so nothing to persist.
5
+ // Everywhere else (iframes fed tokens by a parent, native, and the GNOME/GJS
6
+ // target) the tokens are the only session there is.
7
+ export const persist = isIframe || (!isBrowser && !isServer);
5
8
 
6
9
  export interface AuthTokenState {
7
10
  idToken: string;
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Hand-rolled Better Auth flow for native (Expo) — fetch + expo-web-browser +
3
- * SecureStore, NO better-auth client packages.
2
+ * Hand-rolled Better Auth flow for native (Expo) — fetch + expo-web-browser
3
+ * (ASWebAuthenticationSession / Chrome Custom Tabs) + SecureStore, NO
4
+ * better-auth client packages.
4
5
  *
5
6
  * Why not @better-auth/expo/client: its import graph drags better-auth's
6
7
  * cookies module -> @better-auth/core db schema -> zod into the Hermes bundle,
@@ -12,7 +13,14 @@
12
13
  */
13
14
  export interface ExpoAuthFlow {
14
15
  signIn(provider: string, callbackURL?: string): Promise<void>;
15
- signOut(): Promise<void>;
16
+ /**
17
+ * Ends the Better Auth session AND (when the server exposes the
18
+ * keycloak-sso plugin) the Keycloak SSO session, riding the OIDC
19
+ * end-session endpoint through the same browser that holds the SSO
20
+ * cookie. `postLogoutRedirectUri` is where that browser hands control
21
+ * back (defaults to the app's own runtime URL).
22
+ */
23
+ signOut(postLogoutRedirectUri?: string): Promise<void>;
16
24
  getSession(): Promise<{
17
25
  user?: {
18
26
  id: string;
@@ -26,6 +34,45 @@ export interface ExpoAuthFlow {
26
34
  accessTokenExpiresAt?: string;
27
35
  } | null>;
28
36
  }
37
+ /**
38
+ * Rewrite a loopback host for the device the app actually runs on:
39
+ * `localhost` inside the iOS simulator can resolve to `::1`, where unrelated
40
+ * daemons may squat the port (the reason the web env pins 127.0.0.1), and
41
+ * Android emulators reach the host machine at 10.0.2.2, never loopback.
42
+ * Non-loopback hosts (LAN IPs, real domains) pass through untouched.
43
+ *
44
+ * String-based on purpose: React Native's minimal URL polyfill does not
45
+ * implement the `hostname` SETTER (reads work, mutation throws), so a
46
+ * new-URL-and-assign rewrite silently no-ops on device.
47
+ */
48
+ export declare function rewriteLoopbackHostForNative(configured: string): string;
49
+ /**
50
+ * Loosen a return URL for prefix matching: drop query/fragment and collapse
51
+ * the slash run after the scheme. `Linking.createURL("/")` yields
52
+ * `multiplatform-one:///` (empty authority) but iOS delivers the redirect as
53
+ * `multiplatform-one://?cookie=…` — a strict startsWith between the two
54
+ * misses, leaving the sign-in promise pending forever (observed on the
55
+ * 2026-08-13 simulator run).
56
+ */
57
+ export declare function normalizeForReturnMatch(url: string): string;
58
+ /**
59
+ * Give a bare-scheme URL (`multiplatform-one:///` — what Linking.createURL
60
+ * emits for custom app schemes: empty authority) a host segment. OIDC
61
+ * post-logout redirects need it: Keycloak's redirect builder (Java
62
+ * UriBuilder) throws "empty host" appending the `state` param to an
63
+ * authority-less URI, answering 500 "We are sorry" instead of returning to
64
+ * the app. URLs that already carry an authority (exp://host:port/…) pass
65
+ * through untouched.
66
+ */
67
+ export declare function ensureReturnUrlAuthority(url: string, segment: string): string;
68
+ /**
69
+ * Pull the Better Auth session cookie out of an OAuth return URL without
70
+ * `new URL()`. React Native's URL polyfill throws or drops search on
71
+ * authority-less custom schemes (`multiplatform-one://?cookie=…` — the
72
+ * shape iOS actually delivers). Mirrors `URLSearchParams.get("cookie")`
73
+ * including decodeURIComponent.
74
+ */
75
+ export declare function extractCookieFromReturnUrl(returnUrl: string): string | null;
29
76
  export declare function getActiveExpoAuthFlow(): ExpoAuthFlow | null;
30
77
  export declare function createExpoAuthFlow(baseURL: string, scheme?: string): ExpoAuthFlow;
31
78
  //# sourceMappingURL=expoAuthFlow.native.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"expoAuthFlow.native.d.ts","sourceRoot":"","sources":["../../src/betterAuth/expoAuthFlow.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA6EH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,UAAU,IAAI,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACvF,mEAAmE;IACnE,cAAc,CACZ,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CAC5E;AAOD,wBAAgB,qBAAqB,IAAI,YAAY,GAAG,IAAI,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAkIjF"}
1
+ {"version":3,"file":"expoAuthFlow.native.d.ts","sourceRoot":"","sources":["../../src/betterAuth/expoAuthFlow.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmFH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,UAAU,IAAI,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACvF,mEAAmE;IACnE,cAAc,CACZ,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CAC5E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,4BAA4B,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAGvE;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsB3E;AAqED,wBAAgB,qBAAqB,IAAI,YAAY,GAAG,IAAI,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAgHjF"}
@@ -11,6 +11,12 @@
11
11
  * cookie) and sends `Authorization: Bearer <token>` on every REST request
12
12
  * and socket handshake — the same transport native uses. The bench's
13
13
  * frappe_keycloak auth hook validates the JWT against the realm public key.
14
+ *
15
+ * Failures are classified, not collapsed:
16
+ * - 401/403, or 400 `FAILED_TO_GET_ACCESS_TOKEN` (server already decided
17
+ * the refresh token is dead): signed-out cooldown + session notify.
18
+ * - 5xx `TOKEN_REFRESH_UNAVAILABLE`, other 5xx, network: retry with
19
+ * backoff, keep the session. A Keycloak restart must not look like logout.
14
20
  */
15
21
  /**
16
22
  * Returns a valid Keycloak access token, or undefined when signed out.
@@ -19,9 +25,16 @@
19
25
  * concurrent data request: parallel callers share the in-flight fetch, and a
20
26
  * signed-out answer short-circuits further attempts for a few seconds. A real
21
27
  * sign-in arrives via the OAuth redirect (full navigation), which resets this
22
- * module state anyway.
28
+ * module state anyway. Transient Keycloak failures retry before giving up,
29
+ * and never start the signed-out cooldown.
23
30
  */
24
31
  export declare function getKeycloakBearerToken(): Promise<string | undefined>;
25
32
  /** Drop the cached token (sign-out / fresh sign-in). */
26
33
  export declare function clearKeycloakBearerToken(): void;
34
+ /**
35
+ * Bust the client cache and re-fetch when the tab is focused again, so a
36
+ * front-channel logout (or Keycloak recovery) in another window is noticed
37
+ * without a full reload. Returns an unsubscribe. No-op on the server.
38
+ */
39
+ export declare function watchKeycloakBearerTokenRevalidate(): () => void;
27
40
  //# sourceMappingURL=frappeToken.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frappeToken.d.ts","sourceRoot":"","sources":["../src/frappeToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAUH;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAU1E;AAuCD,wDAAwD;AACxD,wBAAgB,wBAAwB,SAIvC"}
1
+ {"version":3,"file":"frappeToken.d.ts","sourceRoot":"","sources":["../src/frappeToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAgDH;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAW1E;AA4DD,wDAAwD;AACxD,wBAAgB,wBAAwB,SAKvC;AAED;;;;GAIG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,IAAI,CAiB/D"}
@@ -8,8 +8,22 @@
8
8
  * from better-auth's /get-access-token endpoint (which refreshes server-side
9
9
  * when expired), authenticated with the SecureStore cookie jar.
10
10
  */
11
- /** Returns a valid Keycloak access token, or undefined when signed out. */
11
+ /**
12
+ * Returns a valid Keycloak access token, or undefined when signed out.
13
+ *
14
+ * Signed-out clients get a single 401 per cooldown window instead of one per
15
+ * concurrent collection sync: parallel callers share the in-flight fetch, and
16
+ * a signed-out answer short-circuits further attempts for a few seconds. The
17
+ * AuthProvider clears this module state on sign-in / sign-out.
18
+ */
12
19
  export declare function getKeycloakBearerToken(): Promise<string | undefined>;
13
- /** Drop the cached token (sign-out). */
20
+ /** Drop the cached token (sign-out / fresh sign-in). */
14
21
  export declare function clearKeycloakBearerToken(): void;
22
+ /**
23
+ * Bust the client cache and re-fetch when the app returns to the
24
+ * foreground, so a completed ASWebAuthenticationSession (or an SLO in
25
+ * Safari that the Linking listener missed) is noticed without a reload.
26
+ * Returns an unsubscribe. AfterAuth mounts this once.
27
+ */
28
+ export declare function watchKeycloakBearerTokenRevalidate(): () => void;
15
29
  //# sourceMappingURL=frappeToken.native.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"frappeToken.native.d.ts","sourceRoot":"","sources":["../src/frappeToken.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,2EAA2E;AAC3E,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuB1E;AAED,wCAAwC;AACxC,wBAAgB,wBAAwB,SAEvC"}
1
+ {"version":3,"file":"frappeToken.native.d.ts","sourceRoot":"","sources":["../src/frappeToken.native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAaH;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAS1E;AA2BD,wDAAwD;AACxD,wBAAgB,wBAAwB,SAIvC;AAED;;;;;GAKG;AACH,wBAAgB,kCAAkC,IAAI,MAAM,IAAI,CAS/D"}
package/types/index.d.ts CHANGED
@@ -6,5 +6,5 @@ export * from "./provider/index";
6
6
  export * from "./session/index";
7
7
  export * from "./token";
8
8
  export * from "./types";
9
- export { getKeycloakBearerToken } from "./frappeToken";
9
+ export { getKeycloakBearerToken, watchKeycloakBearerTokenRevalidate } from "./frappeToken";
10
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,kCAAkC,EAAE,MAAM,eAAe,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/keycloak/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,cAAc,MAAM,gCAAgC,CAAC;AAKjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAI7F,qBAAa,QAAS,SAAQ,YAAY;IACxC,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,YAAY,GAAG,OAAO;IAcrE,KAAK,CAAC,OAAO,GAAE,oBAAyB;IAgBxC,MAAM,CAAC,OAAO,GAAE,qBAA0B;CAqBjD;AAED,wBAAgB,WAAW,gCAwB1B;AAED,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/keycloak/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,cAAc,MAAM,gCAAgC,CAAC;AAMjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAI7F,qBAAa,QAAS,SAAQ,YAAY;IACxC,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,YAAY,GAAG,OAAO;IAcrE,KAAK,CAAC,OAAO,GAAE,oBAAyB;IAgBxC,MAAM,CAAC,OAAO,GAAE,qBAA0B;CAqCjD;AAED,wBAAgB,WAAW,gCAwB1B;AAED,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.webext.d.ts","sourceRoot":"","sources":["../../src/keycloak/index.webext.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,cAAc,MAAM,gCAAgC,CAAC;AAKjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAG7F,qBAAa,QAAS,SAAQ,YAAY;IACxC,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,YAAY;IAQ3D,KAAK,CAAC,OAAO,GAAE,oBAAyB;IAgBxC,MAAM,CAAC,OAAO,GAAE,qBAA0B;CAiBjD;AAED,wBAAgB,WAAW,4CAK1B;AAED,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.webext.d.ts","sourceRoot":"","sources":["../../src/keycloak/index.webext.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,cAAc,MAAM,gCAAgC,CAAC;AAMjE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAG7F,qBAAa,QAAS,SAAQ,YAAY;IACxC,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,cAAc,GAAG,YAAY;IAQ3D,KAAK,CAAC,OAAO,GAAE,oBAAyB;IAgBxC,MAAM,CAAC,OAAO,GAAE,qBAA0B;CAgCjD;AAED,wBAAgB,WAAW,4CAK1B;AAED,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * File: /src/oauth/authorizationUrl.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ */
5
+ import type { OidcEndpoints } from "./endpoints";
6
+ export interface AuthorizationUrlParams {
7
+ endpoints: OidcEndpoints;
8
+ clientId: string;
9
+ redirectUri: string;
10
+ state: string;
11
+ codeChallenge: string;
12
+ /**
13
+ * Echoed in the id_token's `nonce` claim (OIDC Core §3.1.2.1). Required
14
+ * rather than optional: an authorization request built without one yields
15
+ * an ID token with nothing to bind it to this login.
16
+ */
17
+ nonce: string;
18
+ /** Defaults to the OIDC minimum plus the claims BaseKeycloak reads. */
19
+ scopes?: string[];
20
+ prompt?: "none" | "login" | "consent" | "select_account";
21
+ loginHint?: string;
22
+ }
23
+ export declare const defaultScopes: string[];
24
+ export declare function buildAuthorizationUrl({ endpoints, clientId, redirectUri, state, codeChallenge, nonce, scopes, prompt, loginHint, }: AuthorizationUrlParams): string;
25
+ export interface LogoutUrlParams {
26
+ endpoints: OidcEndpoints;
27
+ clientId: string;
28
+ idToken?: string;
29
+ postLogoutRedirectUri?: string;
30
+ }
31
+ export declare function buildLogoutUrl({ endpoints, clientId, idToken, postLogoutRedirectUri, }: LogoutUrlParams): string;
32
+ //# sourceMappingURL=authorizationUrl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authorizationUrl.d.ts","sourceRoot":"","sources":["../../src/oauth/authorizationUrl.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAGjD,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,aAAa,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,gBAAgB,CAAC;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,aAAa,UAAiC,CAAC;AAE5D,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,QAAQ,EACR,WAAW,EACX,KAAK,EACL,aAAa,EACb,KAAK,EACL,MAAsB,EACtB,MAAM,EACN,SAAS,GACV,EAAE,sBAAsB,GAAG,MAAM,CAajC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,aAAa,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,QAAQ,EACR,OAAO,EACP,qBAAqB,GACtB,EAAE,eAAe,GAAG,MAAM,CAQ1B"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * File: /src/oauth/callback.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ */
5
+ export type CallbackQuery = Record<string, string | undefined>;
6
+ /**
7
+ * Is this request the redirect for the login that generated `expectedState`?
8
+ *
9
+ * Anything on the machine can hit 127.0.0.1, so the listener needs to tell a
10
+ * stray local request from the real callback *before* it decides it is done
11
+ * waiting. A request that fails this is noise: it gets a 404 and the listener
12
+ * keeps listening. Without that distinction, any unprivileged local process
13
+ * that races the browser to the port can deny sign-in — it cannot get a code
14
+ * exchanged, because `parseAuthorizationCallback` checks state again, but it
15
+ * can make the real callback arrive at a closed socket.
16
+ */
17
+ export declare function isCallbackForState(query: CallbackQuery, expectedState: string): boolean;
18
+ /**
19
+ * Validate the redirect's query and return the authorization code.
20
+ *
21
+ * The state check is the CSRF defence for the loopback listener: anything on
22
+ * the machine can hit 127.0.0.1, so a callback that does not echo the state we
23
+ * generated is rejected rather than exchanged.
24
+ */
25
+ export declare function parseAuthorizationCallback(query: CallbackQuery, expectedState: string): string;
26
+ //# sourceMappingURL=callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../../src/oauth/callback.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE/D;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAEvF;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CAc9F"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * File: /src/oauth/callbackPage.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ *
5
+ * The HTML the loopback listener serves back into the browser tab the
6
+ * redirect lands in.
7
+ *
8
+ * It lives in the OAuth core rather than in the GJS listener for two reasons:
9
+ * markup assembly is pure, so it can be unit tested in CI where nothing runs
10
+ * gjs; and putting the only place this flow emits HTML behind one escaped
11
+ * renderer means there is a single thing to audit.
12
+ *
13
+ * Two rules, both load-bearing:
14
+ *
15
+ * 1. **Nothing the authorization server sent is rendered.** The failure
16
+ * page's copy is fixed. Keycloak's `error_description` arrives on a
17
+ * redirect that any local process can forge, so treating it as markup
18
+ * turned an error page into a script-injection sink; the app still
19
+ * receives the full text through the callback query and surfaces it as
20
+ * an `OAuthError` where it is a string, not a document.
21
+ * 2. **Everything interpolated is escaped anyway.** Belt and braces: the
22
+ * inputs are constants today, and this way they can stop being
23
+ * constants without that being a vulnerability.
24
+ */
25
+ /** Escape text for an HTML text node or a double-quoted attribute. */
26
+ export declare function escapeHtml(value: string): string;
27
+ export interface CallbackPageContent {
28
+ heading: string;
29
+ detail: string;
30
+ }
31
+ export declare const callbackPageSuccess: CallbackPageContent;
32
+ export declare const callbackPageFailure: CallbackPageContent;
33
+ export declare function renderCallbackPage({ heading, detail }: CallbackPageContent): string;
34
+ //# sourceMappingURL=callbackPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callbackPage.d.ts","sourceRoot":"","sources":["../../src/oauth/callbackPage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,mBAAmB,EAAE,mBAGjC,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,mBAGjC,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,mBAAmB,GAAG,MAAM,CAMnF"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * File: /src/oauth/endpoints.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ *
5
+ * Keycloak's OIDC endpoints are positional under /realms/<realm>, so they can
6
+ * be derived without a discovery round trip. Deriving beats fetching
7
+ * .well-known here: the GNOME target builds these before it has a working
8
+ * token, and one less network hop is one less failure mode at startup.
9
+ *
10
+ * This is also the transport-security choke point, and `OidcEndpoints` is
11
+ * branded so that it is one by construction rather than by convention: no
12
+ * hand-written object satisfies the type, so every code path that talks to
13
+ * Keycloak — the authorization URL, the token exchange, the refresh, the
14
+ * back-channel logout — necessarily derives its URLs from `keycloakEndpoints`,
15
+ * and rejecting a plaintext issuer here rejects it everywhere. That matters
16
+ * more than usual for this flow:
17
+ * `validateIdToken` skips signature verification on the strength of OIDC Core
18
+ * §3.1.3.7, whose precondition is that the token arrived directly from the
19
+ * token endpoint *over TLS*. Without an enforced https floor that precondition
20
+ * is an assumption, and an assumption is not a control.
21
+ */
22
+ declare const oidcEndpointsBrand: unique symbol;
23
+ export interface OidcEndpoints {
24
+ /**
25
+ * Phantom brand — in the type, never on the object. It is what turns the
26
+ * choke point described above from a convention into something the
27
+ * compiler holds: an object literal cannot satisfy this interface, so the
28
+ * only way a caller gets an `OidcEndpoints` is out of `keycloakEndpoints`,
29
+ * which runs `assertSecureIssuer` first. Without it a consumer could hand
30
+ * `exchangeAuthorizationCode` a plaintext `token` URL of its own making
31
+ * and the https floor would never have been consulted.
32
+ */
33
+ readonly [oidcEndpointsBrand]: true;
34
+ /** The `iss` claim an ID token minted by this realm must carry. */
35
+ issuer: string;
36
+ authorization: string;
37
+ token: string;
38
+ endSession: string;
39
+ userinfo: string;
40
+ revocation: string;
41
+ }
42
+ export interface RealmLocation {
43
+ /** Keycloak base URL, e.g. `https://kc.example.com`. */
44
+ url: string;
45
+ realm: string;
46
+ /**
47
+ * Development-only escape hatch for a Keycloak running on the same machine
48
+ * — `http://localhost:8080`, the docker realm, a stand-in IdP in a test.
49
+ *
50
+ * Deliberately narrow: it permits `http` for **loopback hosts only**
51
+ * (`localhost`, `127.0.0.0/8`, `::1`). A plaintext URL pointing anywhere
52
+ * else stays rejected whatever this is set to, because the traffic would
53
+ * leave the machine. Deliberately explicit: there is no silent fallback and
54
+ * no environment sniffing, so a build that talks plaintext to a real realm
55
+ * has to have said so in its own source.
56
+ */
57
+ allowInsecureLoopbackHttp?: boolean;
58
+ }
59
+ /**
60
+ * Reject any issuer the OIDC Core §3.1.3.7 carve-out would not cover.
61
+ *
62
+ * Exported so a caller can check a configured URL before it builds anything
63
+ * from it — the message names the offending host rather than just failing.
64
+ */
65
+ export declare function assertSecureIssuer({ url, allowInsecureLoopbackHttp }: RealmLocation): void;
66
+ export declare function keycloakEndpoints(location: RealmLocation): OidcEndpoints;
67
+ export {};
68
+ //# sourceMappingURL=endpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../../src/oauth/endpoints.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,CAAC,MAAM,kBAAkB,EAAE,OAAO,MAAM,CAAC;AAEhD,MAAM,WAAW,aAAa;IAC5B;;;;;;;;OAQG;IACH,QAAQ,CAAC,CAAC,kBAAkB,CAAC,EAAE,IAAI,CAAC;IACpC,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,wDAAwD;IACxD,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;;OAUG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAuBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,GAAG,EAAE,yBAAyB,EAAE,EAAE,aAAa,GAAG,IAAI,CA0B1F;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,aAAa,GAAG,aAAa,CAexE"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * File: /src/oauth/errors.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ */
5
+ /** An OAuth 2.0 / OIDC protocol failure, carrying the spec's `error` code. */
6
+ export declare class OAuthError extends Error {
7
+ /** The `error` field from the authorization or token response. */
8
+ readonly code: string;
9
+ /** The `error_description` field, when the server sent one. */
10
+ readonly description?: string | undefined;
11
+ constructor(
12
+ /** The `error` field from the authorization or token response. */
13
+ code: string, message?: string,
14
+ /** The `error_description` field, when the server sent one. */
15
+ description?: string | undefined);
16
+ }
17
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/oauth/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,8EAA8E;AAC9E,qBAAa,UAAW,SAAQ,KAAK;IAEjC,kEAAkE;aAClD,IAAI,EAAE,MAAM;IAE5B,+DAA+D;aAC/C,WAAW,CAAC,EAAE,MAAM;;IAJpC,kEAAkE;IAClD,IAAI,EAAE,MAAM,EAC5B,OAAO,CAAC,EAAE,MAAM;IAChB,+DAA+D;IAC/C,WAAW,CAAC,EAAE,MAAM,YAAA;CAKvC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * File: /src/oauth/form.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ *
5
+ * application/x-www-form-urlencoded serialization.
6
+ *
7
+ * URLSearchParams would do this in one line, but GJS has no such global —
8
+ * the GNOME app only gets one from the react-gnome polyfills, and the auth
9
+ * entries deliberately run without them. Twelve lines here keeps the OAuth
10
+ * core dependent on nothing but the language.
11
+ *
12
+ * Matches URLSearchParams byte for byte, including the space-as-"+" rule that
13
+ * the form-urlencoded media type requires.
14
+ */
15
+ export declare function encodeForm(params: Record<string, string>): string;
16
+ //# sourceMappingURL=form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/oauth/form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAIjE"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * File: /src/oauth/idToken.ts
3
+ * Project: @multiplatform.one/keycloak
4
+ *
5
+ * ID token validation for the loopback flow (OIDC Core §3.1.3.7).
6
+ *
7
+ * ## What is checked, and what is deliberately not
8
+ *
9
+ * Checked: `iss`, `aud`, `azp`, `exp`, `iat`, `nbf`, `nonce`, `sub`, and that
10
+ * the header does not claim `alg: none`.
11
+ *
12
+ * **Not checked: the signature.** This is a deliberate decision, not an
13
+ * oversight, and it rests on a precondition that is enforced rather than
14
+ * assumed. OIDC Core §3.1.3.7 rule 6 says a client MAY skip signature
15
+ * validation when the ID token is received "directly from the Token Endpoint"
16
+ * over a TLS-protected channel, because TLS server authentication already
17
+ * proves who minted it. Both halves of that are true here and neither is left
18
+ * to chance:
19
+ *
20
+ * 1. *Directly from the token endpoint.* `validateIdToken` is only ever
21
+ * called from `tokenExchange.ts`, on the parsed body of a POST this
22
+ * process just made to `endpoints.token`. No ID token from a redirect,
23
+ * a front channel or a caller reaches it — there is no implicit flow
24
+ * here and no `response_type` that returns a token on the redirect.
25
+ * 2. *Over TLS.* `keycloakEndpoints` refuses to derive a token endpoint for
26
+ * a plaintext issuer unless the host is loopback **and** the caller
27
+ * passed `allowInsecureLoopbackHttp`. See endpoints.ts.
28
+ *
29
+ * The alternative is a JWKS fetch plus RS256 verification, and it is not
30
+ * available: GJS has no `node:crypto` and no WebCrypto (`globalThis.crypto`
31
+ * is undefined as of gjs 1.88), so RSA verification would mean hand-rolling
32
+ * modular exponentiation over bignums in JavaScript. Hand-rolled RSA in an
33
+ * auth path is a worse risk than the carve-out the spec explicitly grants.
34
+ *
35
+ * If this code ever accepts an ID token from anywhere other than a direct
36
+ * token-endpoint response — a front-channel redirect, a push from a server,
37
+ * a cached token from another process — the carve-out stops applying and the
38
+ * signature has to be verified for real.
39
+ *
40
+ * ## What the carve-out does not excuse
41
+ *
42
+ * Nothing above says anything about the claims. A validly-signed token minted
43
+ * for a different client in the same realm, or one that expired last week, is
44
+ * still the wrong token; `azp`/`aud` and `exp` are what catch those, and they
45
+ * are checked unconditionally.
46
+ */
47
+ export interface IdTokenClaims {
48
+ iss?: string;
49
+ aud?: string | string[];
50
+ azp?: string;
51
+ exp?: number;
52
+ iat?: number;
53
+ nbf?: number;
54
+ nonce?: string;
55
+ sub?: string;
56
+ [claim: string]: unknown;
57
+ }
58
+ export interface IdTokenValidation {
59
+ /** `endpoints.issuer` — the exact string the `iss` claim must equal. */
60
+ issuer: string;
61
+ clientId: string;
62
+ /**
63
+ * The nonce sent on the authorization request. When supplied the token's
64
+ * `nonce` must match it exactly, which is what stops a token captured from
65
+ * one login being replayed into another.
66
+ *
67
+ * Omitted only on the refresh grant, where there is no fresh authorization
68
+ * request to bind to and the token store does not carry the original nonce
69
+ * across process restarts. The refreshed token is still bound to this
70
+ * client by `azp`/`aud` and to this realm by `iss`.
71
+ */
72
+ nonce?: string;
73
+ /** Epoch seconds. Injected by tests; defaults to the wall clock. */
74
+ now?: number;
75
+ /** Allowance for clock skew between this machine and Keycloak. */
76
+ clockToleranceSeconds?: number;
77
+ }
78
+ /**
79
+ * Validate an ID token that came straight off a token-endpoint response, and
80
+ * return its claims. Throws `OAuthError("invalid_id_token")` on any failure.
81
+ */
82
+ export declare function validateIdToken(idToken: string, validation: IdTokenValidation): IdTokenClaims;
83
+ //# sourceMappingURL=idToken.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idToken.d.ts","sourceRoot":"","sources":["../../src/oauth/idToken.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAKH,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAiBD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,iBAAiB,GAAG,aAAa,CAkD7F"}
@@ -0,0 +1,11 @@
1
+ export * from "./authorizationUrl";
2
+ export * from "./callback";
3
+ export * from "./callbackPage";
4
+ export * from "./endpoints";
5
+ export * from "./errors";
6
+ export * from "./idToken";
7
+ export * from "./jwt";
8
+ export * from "./loopbackFlow";
9
+ export * from "./pkce";
10
+ export * from "./tokenExchange";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/oauth/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC;AAC5B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,OAAO,CAAC;AACtB,cAAc,gBAAgB,CAAC;AAC/B,cAAc,QAAQ,CAAC;AACvB,cAAc,iBAAiB,CAAC"}