@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.
- 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
package/dist/esm/index.js
CHANGED
|
@@ -29,8 +29,8 @@ function useTokensFromQuery() {
|
|
|
29
29
|
|
|
30
30
|
// src/state.ts
|
|
31
31
|
import { createStore, useStore } from "@multiplatform.one/store";
|
|
32
|
-
import { isBrowser, isIframe as isIframe2, isServer
|
|
33
|
-
var persist = isIframe2 ||
|
|
32
|
+
import { isBrowser, isIframe as isIframe2, isServer } from "@multiplatform.one/platform";
|
|
33
|
+
var persist = isIframe2 || !isBrowser && !isServer;
|
|
34
34
|
var authStore = createStore(
|
|
35
35
|
{ idToken: "", refreshToken: "", token: "" },
|
|
36
36
|
{ name: "auth", persist }
|
|
@@ -116,12 +116,122 @@ function getAuthClient(baseURL) {
|
|
|
116
116
|
return _client;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// src/frappeToken.ts
|
|
120
|
+
var cached;
|
|
121
|
+
var pending;
|
|
122
|
+
var signedOutUntil = 0;
|
|
123
|
+
var unavailableUntil = 0;
|
|
124
|
+
var SIGNED_OUT_COOLDOWN_MS = 15e3;
|
|
125
|
+
var TRANSIENT_COOLDOWN_MS = 3e3;
|
|
126
|
+
var RETRY_DELAYS_MS = [0, 400, 1e3];
|
|
127
|
+
var FAILED_TO_GET_ACCESS_TOKEN = "FAILED_TO_GET_ACCESS_TOKEN";
|
|
128
|
+
function classifyAccessTokenResponse(status, body) {
|
|
129
|
+
if (status === 401 || status === 403) return "signed-out";
|
|
130
|
+
if (status === 400 && body?.code === FAILED_TO_GET_ACCESS_TOKEN) return "signed-out";
|
|
131
|
+
return "transient";
|
|
132
|
+
}
|
|
133
|
+
function notifySessionInvalidated() {
|
|
134
|
+
try {
|
|
135
|
+
const client = getAuthClient();
|
|
136
|
+
client.$store?.notify?.("$sessionSignal");
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function sleep(ms) {
|
|
141
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
142
|
+
}
|
|
143
|
+
async function getKeycloakBearerToken() {
|
|
144
|
+
if (typeof window === "undefined") return void 0;
|
|
145
|
+
if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
|
|
146
|
+
if (Date.now() < signedOutUntil) return void 0;
|
|
147
|
+
if (Date.now() < unavailableUntil) return void 0;
|
|
148
|
+
if (pending) return pending;
|
|
149
|
+
pending = fetchToken().finally(() => {
|
|
150
|
+
pending = void 0;
|
|
151
|
+
});
|
|
152
|
+
return pending;
|
|
153
|
+
}
|
|
154
|
+
async function fetchTokenOnce() {
|
|
155
|
+
try {
|
|
156
|
+
const res = await fetch(`${window.location.origin}/api/auth/get-access-token`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
credentials: "include",
|
|
159
|
+
headers: { "Content-Type": "application/json" },
|
|
160
|
+
body: JSON.stringify({ providerId: "keycloak" })
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
const body = await res.json().catch(() => null);
|
|
164
|
+
return { ok: false, kind: classifyAccessTokenResponse(res.status, body) };
|
|
165
|
+
}
|
|
166
|
+
const data = await res.json().catch(() => null);
|
|
167
|
+
if (!data?.accessToken) {
|
|
168
|
+
return { ok: false, kind: "signed-out" };
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
token: data.accessToken,
|
|
173
|
+
expiresAt: data.accessTokenExpiresAt ? new Date(data.accessTokenExpiresAt).getTime() : Date.now() + 6e4
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
return { ok: false, kind: "transient" };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function fetchToken() {
|
|
180
|
+
let last = "transient";
|
|
181
|
+
for (let i = 0; i < RETRY_DELAYS_MS.length; i++) {
|
|
182
|
+
const wait = RETRY_DELAYS_MS[i];
|
|
183
|
+
if (wait) await sleep(wait);
|
|
184
|
+
const result = await fetchTokenOnce();
|
|
185
|
+
if (result.ok) {
|
|
186
|
+
cached = { token: result.token, expiresAt: result.expiresAt };
|
|
187
|
+
return result.token;
|
|
188
|
+
}
|
|
189
|
+
last = result.kind;
|
|
190
|
+
if (result.kind === "signed-out") {
|
|
191
|
+
cached = void 0;
|
|
192
|
+
signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
|
|
193
|
+
notifySessionInvalidated();
|
|
194
|
+
return void 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
cached = void 0;
|
|
198
|
+
if (last === "transient") {
|
|
199
|
+
unavailableUntil = Date.now() + TRANSIENT_COOLDOWN_MS;
|
|
200
|
+
}
|
|
201
|
+
return void 0;
|
|
202
|
+
}
|
|
203
|
+
function clearKeycloakBearerToken() {
|
|
204
|
+
cached = void 0;
|
|
205
|
+
pending = void 0;
|
|
206
|
+
signedOutUntil = 0;
|
|
207
|
+
unavailableUntil = 0;
|
|
208
|
+
}
|
|
209
|
+
function watchKeycloakBearerTokenRevalidate() {
|
|
210
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
211
|
+
return () => {
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const onVisible = () => {
|
|
215
|
+
if (document.visibilityState !== "visible") return;
|
|
216
|
+
cached = void 0;
|
|
217
|
+
signedOutUntil = 0;
|
|
218
|
+
unavailableUntil = 0;
|
|
219
|
+
void getKeycloakBearerToken();
|
|
220
|
+
};
|
|
221
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
222
|
+
window.addEventListener("focus", onVisible);
|
|
223
|
+
return () => {
|
|
224
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
225
|
+
window.removeEventListener("focus", onVisible);
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
119
229
|
// src/session/index.ts
|
|
120
|
-
import { isBrowser as isBrowser2
|
|
230
|
+
import { isBrowser as isBrowser2 } from "@multiplatform.one/platform";
|
|
121
231
|
var useSession = (_options) => {
|
|
122
232
|
return { session: null, status: "unauthenticated" };
|
|
123
233
|
};
|
|
124
|
-
if (isBrowser2
|
|
234
|
+
if (isBrowser2) {
|
|
125
235
|
useSession = (_options) => {
|
|
126
236
|
const client = getAuthClient();
|
|
127
237
|
const { data, isPending, error: _error } = client.useSession();
|
|
@@ -312,21 +422,29 @@ var Keycloak = class extends BaseKeycloak {
|
|
|
312
422
|
async logout(options = {}) {
|
|
313
423
|
if (this._keycloakClient) {
|
|
314
424
|
await this._keycloakClient.logout(options);
|
|
425
|
+
} else if (this._logout) {
|
|
426
|
+
await this._logout(options);
|
|
427
|
+
} else if (typeof window !== "undefined" && window.location?.href) {
|
|
428
|
+
clearKeycloakBearerToken();
|
|
429
|
+
const origin = window.location.origin;
|
|
430
|
+
let callbackURL = options.redirectUri || "/";
|
|
431
|
+
try {
|
|
432
|
+
const url = new URL(callbackURL, origin);
|
|
433
|
+
callbackURL = url.origin === origin ? `${url.pathname}${url.search}${url.hash}` || "/" : url.toString();
|
|
434
|
+
} catch {
|
|
435
|
+
callbackURL = "/";
|
|
436
|
+
}
|
|
437
|
+
window.location.href = `${origin}/api/auth/sign-out/sso?callbackURL=${encodeURIComponent(callbackURL)}`;
|
|
438
|
+
return;
|
|
315
439
|
} else if (isBrowser3 && !isServer2) {
|
|
316
440
|
const client = getAuthClient();
|
|
317
441
|
await client.signOut({
|
|
318
442
|
fetchOptions: {
|
|
319
443
|
onSuccess: () => {
|
|
320
|
-
if (options.redirectUri)
|
|
321
|
-
if (isWeb2) window.location.href = options.redirectUri;
|
|
322
|
-
} else {
|
|
323
|
-
if (isWeb2) window.location.reload();
|
|
324
|
-
}
|
|
444
|
+
if (options.redirectUri && isWeb2) window.location.href = options.redirectUri;
|
|
325
445
|
}
|
|
326
446
|
}
|
|
327
447
|
});
|
|
328
|
-
} else {
|
|
329
|
-
await this._logout?.(options);
|
|
330
448
|
}
|
|
331
449
|
this._clear();
|
|
332
450
|
}
|
|
@@ -440,6 +558,7 @@ function AfterAuth({ children, loadingComponent: _loadingComponent }) {
|
|
|
440
558
|
logger.debug("keycloak disabled");
|
|
441
559
|
}
|
|
442
560
|
}, [authConfig.debug, keycloak]);
|
|
561
|
+
useEffect2(() => watchKeycloakBearerTokenRevalidate(), []);
|
|
443
562
|
return /* @__PURE__ */ jsx2(Fragment2, { children });
|
|
444
563
|
}
|
|
445
564
|
|
|
@@ -837,52 +956,6 @@ function KeycloakProvider({
|
|
|
837
956
|
}
|
|
838
957
|
) });
|
|
839
958
|
}
|
|
840
|
-
|
|
841
|
-
// src/frappeToken.ts
|
|
842
|
-
var cached;
|
|
843
|
-
var pending;
|
|
844
|
-
var signedOutUntil = 0;
|
|
845
|
-
var SIGNED_OUT_COOLDOWN_MS = 15e3;
|
|
846
|
-
async function getKeycloakBearerToken() {
|
|
847
|
-
if (typeof window === "undefined") return void 0;
|
|
848
|
-
if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
|
|
849
|
-
if (Date.now() < signedOutUntil) return void 0;
|
|
850
|
-
if (pending) return pending;
|
|
851
|
-
pending = fetchToken().finally(() => {
|
|
852
|
-
pending = void 0;
|
|
853
|
-
});
|
|
854
|
-
return pending;
|
|
855
|
-
}
|
|
856
|
-
async function fetchToken() {
|
|
857
|
-
try {
|
|
858
|
-
const res = await fetch(`${window.location.origin}/api/auth/get-access-token`, {
|
|
859
|
-
method: "POST",
|
|
860
|
-
credentials: "include",
|
|
861
|
-
headers: { "Content-Type": "application/json" },
|
|
862
|
-
body: JSON.stringify({ providerId: "keycloak" })
|
|
863
|
-
});
|
|
864
|
-
if (!res.ok) {
|
|
865
|
-
cached = void 0;
|
|
866
|
-
signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
|
|
867
|
-
return void 0;
|
|
868
|
-
}
|
|
869
|
-
const data = await res.json().catch(() => null);
|
|
870
|
-
if (!data?.accessToken) {
|
|
871
|
-
cached = void 0;
|
|
872
|
-
signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
|
|
873
|
-
return void 0;
|
|
874
|
-
}
|
|
875
|
-
cached = {
|
|
876
|
-
token: data.accessToken,
|
|
877
|
-
expiresAt: data.accessTokenExpiresAt ? new Date(data.accessTokenExpiresAt).getTime() : Date.now() + 6e4
|
|
878
|
-
};
|
|
879
|
-
return cached.token;
|
|
880
|
-
} catch {
|
|
881
|
-
cached = void 0;
|
|
882
|
-
signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
|
|
883
|
-
return void 0;
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
959
|
export {
|
|
887
960
|
AfterAuth,
|
|
888
961
|
AuthConfigContext,
|
|
@@ -905,5 +978,6 @@ export {
|
|
|
905
978
|
useTokensFromQuery,
|
|
906
979
|
useTokensFromStore,
|
|
907
980
|
validOrRefreshableToken,
|
|
981
|
+
watchKeycloakBearerTokenRevalidate,
|
|
908
982
|
withAuthenticated
|
|
909
983
|
};
|
package/dist/esm/index.native.js
CHANGED
|
@@ -21,8 +21,8 @@ var useTokensFromQuery = () => false;
|
|
|
21
21
|
|
|
22
22
|
// src/state.ts
|
|
23
23
|
import { createStore, useStore } from "@multiplatform.one/store";
|
|
24
|
-
import { isBrowser, isIframe, isServer
|
|
25
|
-
var persist = isIframe ||
|
|
24
|
+
import { isBrowser, isIframe, isServer } from "@multiplatform.one/platform";
|
|
25
|
+
var persist = isIframe || !isBrowser && !isServer;
|
|
26
26
|
var authStore = createStore(
|
|
27
27
|
{ idToken: "", refreshToken: "", token: "" },
|
|
28
28
|
{ name: "auth", persist }
|
|
@@ -285,82 +285,19 @@ function withAuthenticated(Component, options = {}) {
|
|
|
285
285
|
// src/provider/AfterAuth.tsx
|
|
286
286
|
import { logger } from "@multiplatform.one/logger";
|
|
287
287
|
import { useEffect as useEffect2 } from "react";
|
|
288
|
-
import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
|
|
289
|
-
function AfterAuth({ children, loadingComponent: _loadingComponent }) {
|
|
290
|
-
const authConfig = useAuthConfig();
|
|
291
|
-
const authStore2 = useAuthStore();
|
|
292
|
-
const keycloak = useKeycloak();
|
|
293
|
-
useEffect2(() => {
|
|
294
|
-
if (!persist || !keycloak?.authenticated) return;
|
|
295
|
-
if (keycloak.token) {
|
|
296
|
-
authStore2.setTokens({
|
|
297
|
-
token: keycloak.token,
|
|
298
|
-
...keycloak.idToken && { idToken: keycloak.idToken },
|
|
299
|
-
...keycloak.refreshToken && {
|
|
300
|
-
refreshToken: keycloak.refreshToken
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}, [
|
|
305
|
-
authStore2,
|
|
306
|
-
persist,
|
|
307
|
-
keycloak?.authenticated,
|
|
308
|
-
keycloak?.token,
|
|
309
|
-
keycloak?.idToken,
|
|
310
|
-
keycloak?.refreshToken
|
|
311
|
-
]);
|
|
312
|
-
useEffect2(() => {
|
|
313
|
-
if (authConfig.debug && keycloak?.token) {
|
|
314
|
-
logger.debug("token", keycloak.token);
|
|
315
|
-
}
|
|
316
|
-
}, [authConfig.debug, keycloak?.token]);
|
|
317
|
-
useEffect2(() => {
|
|
318
|
-
if (authConfig.debug && keycloak?.idToken) {
|
|
319
|
-
logger.debug("idToken", keycloak.idToken);
|
|
320
|
-
}
|
|
321
|
-
}, [authConfig.debug, keycloak?.idToken]);
|
|
322
|
-
useEffect2(() => {
|
|
323
|
-
if (authConfig.debug && keycloak?.refreshToken) {
|
|
324
|
-
logger.debug("refreshToken", keycloak.refreshToken);
|
|
325
|
-
}
|
|
326
|
-
}, [authConfig.debug, keycloak?.refreshToken]);
|
|
327
|
-
useEffect2(() => {
|
|
328
|
-
if (authConfig.debug && keycloak?.authenticated) {
|
|
329
|
-
logger.debug("authenticated", keycloak.authenticated);
|
|
330
|
-
}
|
|
331
|
-
}, [authConfig.debug, keycloak?.authenticated]);
|
|
332
|
-
useEffect2(() => {
|
|
333
|
-
if (authConfig.debug && keycloak === null) {
|
|
334
|
-
logger.debug("keycloak disabled");
|
|
335
|
-
}
|
|
336
|
-
}, [authConfig.debug, keycloak]);
|
|
337
|
-
return /* @__PURE__ */ jsx2(Fragment2, { children });
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// src/provider/authProvider/index.native.tsx
|
|
341
|
-
import { useEffect as useEffect3, useMemo, useState } from "react";
|
|
342
|
-
import * as Linking2 from "expo-linking";
|
|
343
288
|
|
|
344
|
-
// src/
|
|
345
|
-
import {
|
|
346
|
-
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
347
|
-
function Loading({ loadingComponent }) {
|
|
348
|
-
const { debug } = useAuthConfig();
|
|
349
|
-
const LoadingComponent = loadingComponent;
|
|
350
|
-
if (typeof LoadingComponent === "undefined") {
|
|
351
|
-
return /* @__PURE__ */ jsxs(View, { style: { flex: 1, alignItems: "center", justifyContent: "center" }, children: [
|
|
352
|
-
/* @__PURE__ */ jsx3(ActivityIndicator, {}),
|
|
353
|
-
debug ? /* @__PURE__ */ jsx3(Text2, { children: "loading" }) : null
|
|
354
|
-
] });
|
|
355
|
-
}
|
|
356
|
-
return /* @__PURE__ */ jsx3(LoadingComponent, {});
|
|
357
|
-
}
|
|
289
|
+
// src/frappeToken.native.ts
|
|
290
|
+
import { AppState } from "react-native";
|
|
358
291
|
|
|
359
292
|
// src/betterAuth/expoAuthFlow.native.ts
|
|
360
293
|
import { Platform } from "react-native";
|
|
361
294
|
import * as Linking from "expo-linking";
|
|
362
295
|
import * as SecureStore from "expo-secure-store";
|
|
363
296
|
import * as WebBrowser from "expo-web-browser";
|
|
297
|
+
try {
|
|
298
|
+
WebBrowser.maybeCompleteAuthSession?.();
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
364
301
|
var COOKIE_KEY = "better-auth_cookie";
|
|
365
302
|
var SECURE_COOKIE_PREFIX = "__Secure-";
|
|
366
303
|
function readJar() {
|
|
@@ -420,6 +357,78 @@ function getOAuthState() {
|
|
|
420
357
|
}
|
|
421
358
|
return null;
|
|
422
359
|
}
|
|
360
|
+
function rewriteLoopbackHostForNative(configured) {
|
|
361
|
+
const deviceHost = Platform.OS === "android" ? "10.0.2.2" : "127.0.0.1";
|
|
362
|
+
return configured.replace(/^(https?:\/\/)(localhost|127\.0\.0\.1)(?=[:/]|$)/i, `$1${deviceHost}`);
|
|
363
|
+
}
|
|
364
|
+
function normalizeForReturnMatch(url) {
|
|
365
|
+
return url.split("?")[0].split("#")[0].replace(/:\/{2,}/, "://").replace(/\/+$/, "");
|
|
366
|
+
}
|
|
367
|
+
function ensureReturnUrlAuthority(url, segment) {
|
|
368
|
+
return url.replace(/^([a-z0-9.+-]+:)\/{2,3}(?=\?|#|$)/i, `$1//${segment}`);
|
|
369
|
+
}
|
|
370
|
+
function extractCookieFromReturnUrl(returnUrl) {
|
|
371
|
+
const q = returnUrl.indexOf("?");
|
|
372
|
+
if (q === -1) return null;
|
|
373
|
+
const query = returnUrl.slice(q + 1).split("#")[0];
|
|
374
|
+
for (const part of query.split("&")) {
|
|
375
|
+
const eq = part.indexOf("=");
|
|
376
|
+
const key = eq === -1 ? part : part.slice(0, eq);
|
|
377
|
+
let decodedKey;
|
|
378
|
+
try {
|
|
379
|
+
decodedKey = decodeURIComponent(key.replace(/\+/g, " "));
|
|
380
|
+
} catch {
|
|
381
|
+
decodedKey = key;
|
|
382
|
+
}
|
|
383
|
+
if (decodedKey !== "cookie") continue;
|
|
384
|
+
const raw = eq === -1 ? "" : part.slice(eq + 1);
|
|
385
|
+
try {
|
|
386
|
+
return decodeURIComponent(raw.replace(/\+/g, " "));
|
|
387
|
+
} catch {
|
|
388
|
+
return raw;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
async function openAuthBrowser(url, returnUrlBase, options) {
|
|
394
|
+
const returnBase = normalizeForReturnMatch(returnUrlBase);
|
|
395
|
+
let returned;
|
|
396
|
+
const linkSub = Linking.addEventListener("url", ({ url: next }) => {
|
|
397
|
+
if (!next || returned) return;
|
|
398
|
+
if (!normalizeForReturnMatch(next).startsWith(returnBase)) return;
|
|
399
|
+
returned = next;
|
|
400
|
+
try {
|
|
401
|
+
WebBrowser.dismissAuthSession();
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
const timer = options?.timeoutMs ? setTimeout(() => {
|
|
406
|
+
if (returned) return;
|
|
407
|
+
returned = "";
|
|
408
|
+
try {
|
|
409
|
+
WebBrowser.dismissAuthSession();
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
}, options.timeoutMs) : void 0;
|
|
413
|
+
try {
|
|
414
|
+
const result = await WebBrowser.openAuthSessionAsync(url, returnUrlBase, {
|
|
415
|
+
preferEphemeralSession: false
|
|
416
|
+
});
|
|
417
|
+
if (!returned && result.type === "success" && "url" in result && result.url) {
|
|
418
|
+
returned = result.url;
|
|
419
|
+
}
|
|
420
|
+
if (!returned) {
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
422
|
+
}
|
|
423
|
+
return returned || void 0;
|
|
424
|
+
} finally {
|
|
425
|
+
if (timer) clearTimeout(timer);
|
|
426
|
+
try {
|
|
427
|
+
linkSub.remove();
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
423
432
|
var activeFlow = null;
|
|
424
433
|
function getActiveExpoAuthFlow() {
|
|
425
434
|
return activeFlow;
|
|
@@ -461,45 +470,32 @@ function createExpoAuthFlow(baseURL, scheme) {
|
|
|
461
470
|
if (oauthState) params.append("oauthState", oauthState);
|
|
462
471
|
const proxyURL = `${base}/api/auth/expo-authorization-proxy?${params.toString()}`;
|
|
463
472
|
const toBase = to.split("?")[0];
|
|
464
|
-
|
|
465
|
-
if (Platform.OS === "ios") {
|
|
466
|
-
returnUrl = await new Promise((resolve, reject) => {
|
|
467
|
-
let settled = false;
|
|
468
|
-
const linkSub = Linking.addEventListener("url", ({ url }) => {
|
|
469
|
-
if (settled || !url || !url.startsWith(toBase)) return;
|
|
470
|
-
settled = true;
|
|
471
|
-
try {
|
|
472
|
-
linkSub.remove();
|
|
473
|
-
} catch {
|
|
474
|
-
}
|
|
475
|
-
resolve(url);
|
|
476
|
-
});
|
|
477
|
-
Linking.openURL(proxyURL).catch((e) => {
|
|
478
|
-
if (settled) return;
|
|
479
|
-
settled = true;
|
|
480
|
-
try {
|
|
481
|
-
linkSub.remove();
|
|
482
|
-
} catch {
|
|
483
|
-
}
|
|
484
|
-
reject(e instanceof Error ? e : new Error(`sign-in openURL failed: ${e}`));
|
|
485
|
-
});
|
|
486
|
-
});
|
|
487
|
-
} else {
|
|
488
|
-
const result = await WebBrowser.openAuthSessionAsync(proxyURL, toBase, {
|
|
489
|
-
preferEphemeralSession: false
|
|
490
|
-
// keep the SSO + siww nonce cookie across the wallet bounce
|
|
491
|
-
});
|
|
492
|
-
returnUrl = result.type === "success" && "url" in result ? result.url : void 0;
|
|
493
|
-
}
|
|
473
|
+
const returnUrl = await openAuthBrowser(proxyURL, toBase);
|
|
494
474
|
if (!returnUrl) {
|
|
495
475
|
return;
|
|
496
476
|
}
|
|
497
|
-
const cookie =
|
|
477
|
+
const cookie = extractCookieFromReturnUrl(returnUrl);
|
|
498
478
|
if (cookie) absorbSetCookie(cookie);
|
|
499
479
|
},
|
|
500
|
-
async signOut() {
|
|
480
|
+
async signOut(postLogoutRedirectUri) {
|
|
481
|
+
const to = ensureReturnUrlAuthority(
|
|
482
|
+
postLogoutRedirectUri || Linking.createURL("/"),
|
|
483
|
+
"signed-out"
|
|
484
|
+
);
|
|
501
485
|
try {
|
|
502
|
-
await authFetch(
|
|
486
|
+
const res = await authFetch(
|
|
487
|
+
`/sign-out/sso?mode=json&callbackURL=${encodeURIComponent(to)}`,
|
|
488
|
+
{ method: "GET" }
|
|
489
|
+
);
|
|
490
|
+
if (!res.ok) throw new Error(`sign-out/sso failed (${res.status})`);
|
|
491
|
+
const data = await res.json().catch(() => null);
|
|
492
|
+
if (data?.redirect && data.url) {
|
|
493
|
+
await openAuthBrowser(data.url, to.split("?")[0], { timeoutMs: 12e4 }).catch(
|
|
494
|
+
() => void 0
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
await authFetch("/sign-out", { method: "POST", body: "{}" }).catch(() => void 0);
|
|
503
499
|
} finally {
|
|
504
500
|
writeJar({});
|
|
505
501
|
}
|
|
@@ -523,6 +519,131 @@ function createExpoAuthFlow(baseURL, scheme) {
|
|
|
523
519
|
return flow;
|
|
524
520
|
}
|
|
525
521
|
|
|
522
|
+
// src/frappeToken.native.ts
|
|
523
|
+
var cached;
|
|
524
|
+
var pending;
|
|
525
|
+
var signedOutUntil = 0;
|
|
526
|
+
var SIGNED_OUT_COOLDOWN_MS = 15e3;
|
|
527
|
+
async function getKeycloakBearerToken() {
|
|
528
|
+
if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
|
|
529
|
+
if (Date.now() < signedOutUntil) return void 0;
|
|
530
|
+
if (pending) return pending;
|
|
531
|
+
pending = fetchToken().finally(() => {
|
|
532
|
+
pending = void 0;
|
|
533
|
+
});
|
|
534
|
+
return pending;
|
|
535
|
+
}
|
|
536
|
+
async function fetchToken() {
|
|
537
|
+
const flow = getActiveExpoAuthFlow();
|
|
538
|
+
if (!flow) return void 0;
|
|
539
|
+
try {
|
|
540
|
+
const res = await flow.getAccessToken("keycloak");
|
|
541
|
+
if (!res?.accessToken) {
|
|
542
|
+
cached = void 0;
|
|
543
|
+
signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
|
|
544
|
+
return void 0;
|
|
545
|
+
}
|
|
546
|
+
cached = {
|
|
547
|
+
token: res.accessToken,
|
|
548
|
+
expiresAt: res.accessTokenExpiresAt ? new Date(res.accessTokenExpiresAt).getTime() : Date.now() + 6e4
|
|
549
|
+
};
|
|
550
|
+
return cached.token;
|
|
551
|
+
} catch {
|
|
552
|
+
cached = void 0;
|
|
553
|
+
signedOutUntil = Date.now() + SIGNED_OUT_COOLDOWN_MS;
|
|
554
|
+
return void 0;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function clearKeycloakBearerToken() {
|
|
558
|
+
cached = void 0;
|
|
559
|
+
pending = void 0;
|
|
560
|
+
signedOutUntil = 0;
|
|
561
|
+
}
|
|
562
|
+
function watchKeycloakBearerTokenRevalidate() {
|
|
563
|
+
const onChange = (next) => {
|
|
564
|
+
if (next !== "active") return;
|
|
565
|
+
cached = void 0;
|
|
566
|
+
signedOutUntil = 0;
|
|
567
|
+
void getKeycloakBearerToken();
|
|
568
|
+
};
|
|
569
|
+
const sub = AppState.addEventListener("change", onChange);
|
|
570
|
+
return () => sub.remove();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/provider/AfterAuth.tsx
|
|
574
|
+
import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
|
|
575
|
+
function AfterAuth({ children, loadingComponent: _loadingComponent }) {
|
|
576
|
+
const authConfig = useAuthConfig();
|
|
577
|
+
const authStore2 = useAuthStore();
|
|
578
|
+
const keycloak = useKeycloak();
|
|
579
|
+
useEffect2(() => {
|
|
580
|
+
if (!persist || !keycloak?.authenticated) return;
|
|
581
|
+
if (keycloak.token) {
|
|
582
|
+
authStore2.setTokens({
|
|
583
|
+
token: keycloak.token,
|
|
584
|
+
...keycloak.idToken && { idToken: keycloak.idToken },
|
|
585
|
+
...keycloak.refreshToken && {
|
|
586
|
+
refreshToken: keycloak.refreshToken
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}, [
|
|
591
|
+
authStore2,
|
|
592
|
+
persist,
|
|
593
|
+
keycloak?.authenticated,
|
|
594
|
+
keycloak?.token,
|
|
595
|
+
keycloak?.idToken,
|
|
596
|
+
keycloak?.refreshToken
|
|
597
|
+
]);
|
|
598
|
+
useEffect2(() => {
|
|
599
|
+
if (authConfig.debug && keycloak?.token) {
|
|
600
|
+
logger.debug("token", keycloak.token);
|
|
601
|
+
}
|
|
602
|
+
}, [authConfig.debug, keycloak?.token]);
|
|
603
|
+
useEffect2(() => {
|
|
604
|
+
if (authConfig.debug && keycloak?.idToken) {
|
|
605
|
+
logger.debug("idToken", keycloak.idToken);
|
|
606
|
+
}
|
|
607
|
+
}, [authConfig.debug, keycloak?.idToken]);
|
|
608
|
+
useEffect2(() => {
|
|
609
|
+
if (authConfig.debug && keycloak?.refreshToken) {
|
|
610
|
+
logger.debug("refreshToken", keycloak.refreshToken);
|
|
611
|
+
}
|
|
612
|
+
}, [authConfig.debug, keycloak?.refreshToken]);
|
|
613
|
+
useEffect2(() => {
|
|
614
|
+
if (authConfig.debug && keycloak?.authenticated) {
|
|
615
|
+
logger.debug("authenticated", keycloak.authenticated);
|
|
616
|
+
}
|
|
617
|
+
}, [authConfig.debug, keycloak?.authenticated]);
|
|
618
|
+
useEffect2(() => {
|
|
619
|
+
if (authConfig.debug && keycloak === null) {
|
|
620
|
+
logger.debug("keycloak disabled");
|
|
621
|
+
}
|
|
622
|
+
}, [authConfig.debug, keycloak]);
|
|
623
|
+
useEffect2(() => watchKeycloakBearerTokenRevalidate(), []);
|
|
624
|
+
return /* @__PURE__ */ jsx2(Fragment2, { children });
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/provider/authProvider/index.native.tsx
|
|
628
|
+
import { useEffect as useEffect3, useMemo, useState } from "react";
|
|
629
|
+
import { AppState as AppState2 } from "react-native";
|
|
630
|
+
import * as Linking2 from "expo-linking";
|
|
631
|
+
|
|
632
|
+
// src/Loading.tsx
|
|
633
|
+
import { ActivityIndicator, Text as Text2, View } from "react-native";
|
|
634
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
635
|
+
function Loading({ loadingComponent }) {
|
|
636
|
+
const { debug } = useAuthConfig();
|
|
637
|
+
const LoadingComponent = loadingComponent;
|
|
638
|
+
if (typeof LoadingComponent === "undefined") {
|
|
639
|
+
return /* @__PURE__ */ jsxs(View, { style: { flex: 1, alignItems: "center", justifyContent: "center" }, children: [
|
|
640
|
+
/* @__PURE__ */ jsx3(ActivityIndicator, {}),
|
|
641
|
+
debug ? /* @__PURE__ */ jsx3(Text2, { children: "loading" }) : null
|
|
642
|
+
] });
|
|
643
|
+
}
|
|
644
|
+
return /* @__PURE__ */ jsx3(LoadingComponent, {});
|
|
645
|
+
}
|
|
646
|
+
|
|
526
647
|
// src/session/index.native.ts
|
|
527
648
|
import { useSyncExternalStore } from "react";
|
|
528
649
|
var state = { session: null, status: "loading" };
|
|
@@ -560,7 +681,9 @@ function AuthProvider({
|
|
|
560
681
|
const [keycloak, setKeycloak] = useState();
|
|
561
682
|
const [isLoading, setIsLoading] = useState(true);
|
|
562
683
|
const serverBaseUrl = useMemo(() => {
|
|
563
|
-
return
|
|
684
|
+
return rewriteLoopbackHostForNative(
|
|
685
|
+
keycloakConfig.betterAuthBaseUrl || "http://localhost:8000"
|
|
686
|
+
);
|
|
564
687
|
}, [keycloakConfig]);
|
|
565
688
|
const scheme = useMemo(() => keycloakConfig.expoScheme, [keycloakConfig]);
|
|
566
689
|
const flow = useMemo(
|
|
@@ -587,11 +710,14 @@ function AuthProvider({
|
|
|
587
710
|
void 0,
|
|
588
711
|
async (options) => {
|
|
589
712
|
await flow.signIn("keycloak", options.redirectUri || Linking2.createURL("/"));
|
|
713
|
+
clearKeycloakBearerToken();
|
|
590
714
|
await refresh();
|
|
591
715
|
return void 0;
|
|
592
716
|
},
|
|
593
|
-
async (
|
|
594
|
-
|
|
717
|
+
async (options) => {
|
|
718
|
+
const redirectUri = options.redirectUri && options.redirectUri.includes("://") ? options.redirectUri : Linking2.createURL("/");
|
|
719
|
+
await flow.signOut(redirectUri);
|
|
720
|
+
clearKeycloakBearerToken();
|
|
595
721
|
await refresh();
|
|
596
722
|
return void 0;
|
|
597
723
|
}
|
|
@@ -617,8 +743,13 @@ function AuthProvider({
|
|
|
617
743
|
refresh().finally(() => {
|
|
618
744
|
if (alive) setIsLoading(false);
|
|
619
745
|
});
|
|
746
|
+
const onAppState = (next) => {
|
|
747
|
+
if (next === "active") refresh().catch(() => void 0);
|
|
748
|
+
};
|
|
749
|
+
const appSub = AppState2.addEventListener("change", onAppState);
|
|
620
750
|
return () => {
|
|
621
751
|
alive = false;
|
|
752
|
+
appSub.remove();
|
|
622
753
|
};
|
|
623
754
|
}, [disabled, flow, keycloakConfig]);
|
|
624
755
|
if (disabled) return /* @__PURE__ */ jsx4(Fragment3, { children });
|
|
@@ -664,29 +795,6 @@ function KeycloakProvider({
|
|
|
664
795
|
}
|
|
665
796
|
) });
|
|
666
797
|
}
|
|
667
|
-
|
|
668
|
-
// src/frappeToken.native.ts
|
|
669
|
-
var cached;
|
|
670
|
-
async function getKeycloakBearerToken() {
|
|
671
|
-
if (cached && cached.expiresAt - 6e4 > Date.now()) return cached.token;
|
|
672
|
-
const flow = getActiveExpoAuthFlow();
|
|
673
|
-
if (!flow) return void 0;
|
|
674
|
-
try {
|
|
675
|
-
const res = await flow.getAccessToken("keycloak");
|
|
676
|
-
if (!res?.accessToken) {
|
|
677
|
-
cached = void 0;
|
|
678
|
-
return void 0;
|
|
679
|
-
}
|
|
680
|
-
cached = {
|
|
681
|
-
token: res.accessToken,
|
|
682
|
-
expiresAt: res.accessTokenExpiresAt ? new Date(res.accessTokenExpiresAt).getTime() : Date.now() + 6e4
|
|
683
|
-
};
|
|
684
|
-
return cached.token;
|
|
685
|
-
} catch {
|
|
686
|
-
cached = void 0;
|
|
687
|
-
return void 0;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
798
|
export {
|
|
691
799
|
AfterAuth,
|
|
692
800
|
AuthConfigContext,
|
|
@@ -711,5 +819,6 @@ export {
|
|
|
711
819
|
useTokensFromQuery,
|
|
712
820
|
useTokensFromStore,
|
|
713
821
|
validOrRefreshableToken,
|
|
822
|
+
watchKeycloakBearerTokenRevalidate,
|
|
714
823
|
withAuthenticated
|
|
715
824
|
};
|