@multiplatform.one/keycloak 6.7.0 → 7.1.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/cjs/index.cjs +129 -55
- package/dist/cjs/index.native.cjs +239 -130
- package/dist/esm/index.js +131 -57
- package/dist/esm/index.native.js +239 -130
- package/package.json +15 -10
- package/src/betterAuth/expoAuthFlow.native.spec.ts +307 -0
- package/src/betterAuth/expoAuthFlow.native.ts +189 -55
- package/src/frappeToken.native.spec.ts +104 -0
- package/src/frappeToken.native.ts +46 -2
- package/src/frappeToken.spec.ts +141 -0
- package/src/frappeToken.ts +106 -14
- package/src/index.ts +1 -1
- package/src/keycloak/index.ts +24 -7
- package/src/keycloak/index.webext.ts +16 -0
- package/src/oauth/authorizationUrl.spec.ts +83 -0
- package/src/oauth/authorizationUrl.ts +74 -0
- package/src/oauth/callback.spec.ts +72 -0
- package/src/oauth/callback.ts +46 -0
- package/src/oauth/callbackPage.spec.ts +61 -0
- package/src/oauth/callbackPage.ts +57 -0
- package/src/oauth/endpoints.spec.ts +193 -0
- package/src/oauth/endpoints.ts +135 -0
- package/src/oauth/errors.ts +18 -0
- package/src/oauth/form.spec.ts +35 -0
- package/src/oauth/form.ts +30 -0
- package/src/oauth/idToken.spec.ts +191 -0
- package/src/oauth/idToken.ts +153 -0
- package/src/oauth/index.ts +10 -0
- package/src/oauth/jwt.ts +90 -0
- package/src/oauth/loopbackFlow.spec.ts +226 -0
- package/src/oauth/loopbackFlow.ts +114 -0
- package/src/oauth/pkce.spec.ts +60 -0
- package/src/oauth/pkce.ts +80 -0
- package/src/oauth/tokenExchange.spec.ts +331 -0
- package/src/oauth/tokenExchange.ts +232 -0
- package/src/one.ts +30 -3
- package/src/provider/AfterAuth.tsx +7 -0
- package/src/provider/authProvider/index.native.tsx +37 -4
- package/src/session/index.ts +2 -2
- package/src/state.ts +5 -2
- package/types/betterAuth/expoAuthFlow.native.d.ts +50 -3
- package/types/betterAuth/expoAuthFlow.native.d.ts.map +1 -1
- package/types/frappeToken.d.ts +14 -1
- package/types/frappeToken.d.ts.map +1 -1
- package/types/frappeToken.native.d.ts +16 -2
- package/types/frappeToken.native.d.ts.map +1 -1
- package/types/index.d.ts +1 -1
- package/types/index.d.ts.map +1 -1
- package/types/keycloak/index.d.ts.map +1 -1
- package/types/keycloak/index.webext.d.ts.map +1 -1
- package/types/oauth/authorizationUrl.d.ts +32 -0
- package/types/oauth/authorizationUrl.d.ts.map +1 -0
- package/types/oauth/callback.d.ts +26 -0
- package/types/oauth/callback.d.ts.map +1 -0
- package/types/oauth/callbackPage.d.ts +34 -0
- package/types/oauth/callbackPage.d.ts.map +1 -0
- package/types/oauth/endpoints.d.ts +68 -0
- package/types/oauth/endpoints.d.ts.map +1 -0
- package/types/oauth/errors.d.ts +17 -0
- package/types/oauth/errors.d.ts.map +1 -0
- package/types/oauth/form.d.ts +16 -0
- package/types/oauth/form.d.ts.map +1 -0
- package/types/oauth/idToken.d.ts +83 -0
- package/types/oauth/idToken.d.ts.map +1 -0
- package/types/oauth/index.d.ts +11 -0
- package/types/oauth/index.d.ts.map +1 -0
- package/types/oauth/jwt.d.ts +31 -0
- package/types/oauth/jwt.d.ts.map +1 -0
- package/types/oauth/loopbackFlow.d.ts +64 -0
- package/types/oauth/loopbackFlow.d.ts.map +1 -0
- package/types/oauth/pkce.d.ts +49 -0
- package/types/oauth/pkce.d.ts.map +1 -0
- package/types/oauth/tokenExchange.d.ts +89 -0
- package/types/oauth/tokenExchange.d.ts.map +1 -0
- package/types/one.d.ts +7 -0
- package/types/one.d.ts.map +1 -1
- package/types/provider/AfterAuth.d.ts.map +1 -1
- package/types/provider/authProvider/index.native.d.ts.map +1 -1
- 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 {
|
|
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
|
|
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 (
|
|
79
|
-
|
|
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
|
|
package/src/session/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Replaces the previous next-auth/react useSession.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { isBrowser
|
|
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
|
|
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
|
|
2
|
+
import { isBrowser, isIframe, isServer } from "@multiplatform.one/platform";
|
|
3
3
|
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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"}
|
package/types/frappeToken.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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;
|
|
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
|
package/types/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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;
|
|
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"}
|