Skip to content

Commit 750af62

Browse files
committed
feat: support running auth in iFrame
1 parent d2b08fa commit 750af62

File tree

7 files changed

+220
-85
lines changed

7 files changed

+220
-85
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
],
9898
"private": false,
9999
"dependencies": {
100-
"@kinde/js-utils": "0.18.0-0"
100+
"@kinde/js-utils": "0.22.0"
101101
},
102102
"packageManager": "[email protected]+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
103103
}

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/state/KindeProvider.tsx

Lines changed: 179 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
generatePortalUrl,
2828
Role,
2929
GeneratePortalUrlParams,
30+
navigateToKinde,
3031
} from "@kinde/js-utils";
3132
import * as storeState from "./store";
3233
import React, {
@@ -39,7 +40,7 @@ import React, {
3940
import { KindeContext, KindeContextProps } from "./KindeContext";
4041
import { getRedirectUrl } from "../utils/getRedirectUrl";
4142
import packageJson from "../../package.json";
42-
import { ErrorProps, LogoutOptions } from "./types";
43+
import { ErrorProps, LogoutOptions, PopupOptions } from "./types";
4344
import type { RefreshTokenResult } from "@kinde/js-utils";
4445
// TODO: need to look for old token store and convert.
4546
storageSettings.keyPrefix = "";
@@ -98,6 +99,11 @@ type KindeProviderProps = {
9899
callbacks?: KindeCallbacks;
99100
scope?: string;
100101
forceChildrenRender?: boolean;
102+
/**
103+
* When the application is shown in an iFrame, auth will open in a popup window.
104+
* This is the options for the popup window.
105+
*/
106+
popupOptions?: PopupOptions;
101107
};
102108

103109
const defaultCallbacks: KindeCallbacks = {
@@ -131,6 +137,7 @@ export const KindeProvider = ({
131137
callbacks = {},
132138
logoutUri,
133139
forceChildrenRender = false,
140+
popupOptions = {},
134141
}: KindeProviderProps) => {
135142
const mergedCallbacks = { ...defaultCallbacks, ...callbacks };
136143

@@ -189,9 +196,25 @@ export const KindeProvider = ({
189196
IssuerRouteTypes.login,
190197
authProps,
191198
);
192-
document.location = authUrl.url.toString();
199+
200+
try {
201+
navigateToKinde({
202+
url: authUrl.url.toString(),
203+
popupOptions,
204+
handleResult: processAuthResult,
205+
});
206+
} catch (error) {
207+
mergedCallbacks.onError?.(
208+
{
209+
error: "ERR_POPUP",
210+
errorDescription: (error as Error).message,
211+
},
212+
{},
213+
{} as KindeContextProps,
214+
);
215+
}
193216
},
194-
[audience, clientId, redirectUri],
217+
[audience, clientId, redirectUri, popupOptions, mergedCallbacks],
195218
);
196219

197220
const register = useCallback(
@@ -231,7 +254,22 @@ export const KindeProvider = ({
231254
IssuerRouteTypes.register,
232255
authProps,
233256
);
234-
document.location = authUrl.url.toString();
257+
try {
258+
navigateToKinde({
259+
url: authUrl.url.toString(),
260+
popupOptions,
261+
handleResult: processAuthResult,
262+
});
263+
} catch (error) {
264+
mergedCallbacks.onError?.(
265+
{
266+
error: "ERR_POPUP",
267+
errorDescription: (error as Error).message,
268+
},
269+
{},
270+
{} as KindeContextProps,
271+
);
272+
}
235273
} catch (error) {
236274
console.error("Register error:", error);
237275
mergedCallbacks.onError?.(
@@ -244,7 +282,7 @@ export const KindeProvider = ({
244282
);
245283
}
246284
},
247-
[redirectUri],
285+
[redirectUri, popupOptions, mergedCallbacks],
248286
);
249287

250288
const logout = useCallback(async (options?: string | LogoutOptions) => {
@@ -275,11 +313,32 @@ export const KindeProvider = ({
275313
});
276314

277315
await Promise.all([
278-
storeState.memoryStorage.destroySession(),
279-
storeState.localStorage.destroySession(),
316+
storeState.memoryStorage.removeSessionItem(StorageKeys.idToken),
317+
storeState.memoryStorage.removeSessionItem(StorageKeys.accessToken),
318+
storeState.memoryStorage.removeSessionItem(StorageKeys.refreshToken),
319+
storeState.localStorage.removeSessionItem(StorageKeys.refreshToken),
280320
]);
281321

282-
document.location = `${domain}/logout?${params.toString()}`;
322+
await storeState.localStorage.setSessionItem(
323+
storeState.LocalKeys.performingLogout,
324+
"true",
325+
);
326+
327+
try {
328+
await navigateToKinde({
329+
url: `${domain}/logout?${params.toString()}`,
330+
popupOptions,
331+
});
332+
} catch (error) {
333+
mergedCallbacks.onError?.(
334+
{
335+
error: "ERR_POPUP",
336+
errorDescription: (error as Error).message,
337+
},
338+
{},
339+
{} as KindeContextProps,
340+
);
341+
}
283342
} catch (error) {
284343
console.error("Logout error:", error);
285344
mergedCallbacks.onError?.(
@@ -390,6 +449,89 @@ export const KindeProvider = ({
390449
[mergedCallbacks, contextValue],
391450
);
392451

452+
// Function to process authentication result from popup
453+
const processAuthResult = useCallback(
454+
async (searchParams: URLSearchParams) => {
455+
const decoded = atob(searchParams.get("state") || "");
456+
let returnedState: StateWithKinde;
457+
let kindeState: KindeState;
458+
try {
459+
returnedState = JSON.parse(decoded);
460+
kindeState = Object.assign(
461+
returnedState.kinde || { event: PromptTypes.login },
462+
);
463+
} catch (error) {
464+
console.error("Error parsing state:", error);
465+
mergedCallbacks.onError?.(
466+
{
467+
error: "ERR_STATE_PARSE",
468+
errorDescription: String(error),
469+
},
470+
{},
471+
contextValue,
472+
);
473+
returnedState = {} as StateWithKinde;
474+
kindeState = { event: AuthEvent.login };
475+
}
476+
try {
477+
const codeResponse = await exchangeAuthCode({
478+
urlParams: searchParams,
479+
domain,
480+
clientId,
481+
redirectURL: getRedirectUrl(redirectUri),
482+
autoRefresh: true,
483+
onRefresh,
484+
});
485+
486+
if (codeResponse.success) {
487+
const user = await getUserProfile();
488+
if (user) {
489+
setState((val) => ({ ...val, user, isAuthenticated: true }));
490+
mergedCallbacks.onSuccess?.(
491+
user,
492+
{
493+
...returnedState,
494+
kinde: undefined,
495+
},
496+
contextValue,
497+
);
498+
if (mergedCallbacks.onEvent) {
499+
mergedCallbacks.onEvent(
500+
kindeState.event,
501+
{
502+
...returnedState,
503+
kinde: undefined,
504+
},
505+
contextValue,
506+
);
507+
}
508+
}
509+
} else {
510+
mergedCallbacks.onError?.(
511+
{
512+
error: "ERR_CODE_EXCHANGE",
513+
errorDescription: codeResponse.error,
514+
},
515+
returnedState,
516+
contextValue,
517+
);
518+
}
519+
} catch (error) {
520+
mergedCallbacks.onError?.(
521+
{
522+
error: "ERR_POPUP_AUTH",
523+
errorDescription: String(error),
524+
},
525+
returnedState,
526+
contextValue,
527+
);
528+
} finally {
529+
setState((val) => ({ ...val, isLoading: false }));
530+
}
531+
},
532+
[domain, clientId, redirectUri, onRefresh, mergedCallbacks, contextValue],
533+
);
534+
393535
const handleFocus = useCallback(() => {
394536
if (document.visibilityState === "visible" && state.isAuthenticated) {
395537
refreshToken({ domain, clientId, onRefresh }).catch((error) => {
@@ -412,8 +554,6 @@ export const KindeProvider = ({
412554
await checkAuth({ domain, clientId });
413555
initRef.current = true;
414556
const params = new URLSearchParams(window.location.search);
415-
let returnedState: StateWithKinde;
416-
let kindeState: KindeState;
417557

418558
if (params.has("error")) {
419559
const errorCode = params.get("error");
@@ -428,6 +568,17 @@ export const KindeProvider = ({
428568
return;
429569
}
430570

571+
if (
572+
(await storeState.localStorage.getSessionItem(
573+
storeState.LocalKeys.performingLogout,
574+
)) === "true"
575+
) {
576+
await storeState.localStorage.removeSessionItem(
577+
storeState.LocalKeys.performingLogout,
578+
);
579+
window.close();
580+
}
581+
431582
const hasCode = params.has("code");
432583
if (!hasCode) {
433584
try {
@@ -447,77 +598,28 @@ export const KindeProvider = ({
447598
return;
448599
}
449600

450-
const decoded = atob(params.get("state") || "");
451-
452-
try {
453-
returnedState = JSON.parse(decoded);
454-
kindeState = Object.assign(
455-
returnedState.kinde || { event: PromptTypes.login },
456-
);
457-
} catch (error) {
458-
console.error("Error parsing state:", error);
459-
mergedCallbacks.onError?.(
601+
if (window.opener) {
602+
const searchParams = new URLSearchParams(window.location.search);
603+
window.opener.postMessage(
460604
{
461-
error: "ERR_STATE_PARSE",
462-
errorDescription: String(error),
605+
type: "KINDE_AUTH_RESULT",
606+
result: Object.fromEntries(searchParams.entries()),
463607
},
464-
{},
465-
contextValue,
608+
window.location.origin,
466609
);
467-
returnedState = {} as StateWithKinde;
468-
kindeState = { event: AuthEvent.login };
469-
}
470-
try {
471-
const redirectURL = (await storeState.memoryStorage.getSessionItem(
472-
storeState.LocalKeys.redirectUri,
473-
)) as string;
474-
475-
const codeResponse = await exchangeAuthCode({
476-
urlParams: new URLSearchParams(window.location.search),
477-
domain,
478-
clientId,
479-
redirectURL: getRedirectUrl(redirectURL || redirectUri),
480-
autoRefresh: true,
481-
onRefresh,
482-
});
483-
484-
if (codeResponse.success) {
485-
const user = await getUserProfile();
486-
if (user) {
487-
setState((val) => ({ ...val, user, isAuthenticated: true }));
488-
mergedCallbacks.onSuccess?.(
489-
user,
490-
{
491-
...returnedState,
492-
kinde: undefined,
493-
},
494-
contextValue,
495-
);
496-
if (mergedCallbacks.onEvent) {
497-
mergedCallbacks.onEvent(
498-
kindeState.event,
499-
{
500-
...returnedState,
501-
kinde: undefined,
502-
},
503-
contextValue,
504-
);
505-
}
506-
}
507-
} else {
508-
mergedCallbacks.onError?.(
509-
{
510-
error: "ERR_CODE_EXCHANGE",
511-
errorDescription: codeResponse.error,
512-
},
513-
returnedState,
514-
contextValue,
515-
);
516-
}
517-
} finally {
518-
setState((val) => ({ ...val, isLoading: false }));
610+
window.close();
519611
}
520-
}, [clientId, domain, redirectUri, mergedCallbacks, contextValue, onRefresh]);
612+
await processAuthResult(new URLSearchParams(window.location.search));
613+
}, [
614+
clientId,
615+
domain,
616+
redirectUri,
617+
mergedCallbacks,
618+
contextValue,
619+
onRefresh,
620+
login,
621+
processAuthResult,
622+
]);
521623

522624
useEffect(() => {
523625
const mounted = { current: true };

src/state/store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ enum LocalKeys {
1111
audience = "audience",
1212
redirectUri = "redirect_uri",
1313
logoutUri = "logout_uri",
14+
performingLogout = "performing_logout",
1415
}
1516

1617
const memoryStorage = new MemoryStorage<LocalKeys>();
1718
const localStorage = new LocalStorage<LocalKeys>();
19+
1820
// TODO: Resolve type issue
1921
//@ts-expect-error valid assignment
2022
setActiveStorage(memoryStorage);

0 commit comments

Comments
 (0)