Repository: authts/oidc-client-ts (ts)

Issue:
OIDC Client is not capable in an iFrame
I have a login page with a Sign In button hosted within an iFrame. When Sign In button is clicked UserManager.signinRedirect is called. Currently this redirect only applies to the current window (e.g. window.location). It would be nice to have an option to redirect the top window or parent (e.g. window.top.location).

Pull request description:
Adds ability to redirect window top when used within iFrame
<!-- Please link relevant issue numbers or provide context for this change -->
Closes/fixes #572 

### Checklist

- [x] This PR makes changes to the public API <!-- was the API report (docs/oidc-client-ts.api.md) updated by this PR? -->
- [x] I have included links for closing relevant issue numbers


Patch:
diff --git a/docs/oidc-client-ts.api.md b/docs/oidc-client-ts.api.md
index d27df6e0..94869031 100644
--- a/docs/oidc-client-ts.api.md
+++ b/docs/oidc-client-ts.api.md
@@ -543,6 +543,8 @@ export type QuerySessionStatusArgs = IFrameWindowParams & ExtraSigninRequestArgs
 export interface RedirectParams {
     // (undocumented)
     redirectMethod?: "replace" | "assign";
+    // (undocumented)
+    redirectTarget?: "top" | "self";
 }
 
 // @public (undocumented)
@@ -982,6 +984,7 @@ export interface UserManagerSettings extends OidcClientSettings {
     // (undocumented)
     query_status_response_type?: string;
     redirectMethod?: "replace" | "assign";
+    redirectTarget?: "top" | "self";
     revokeTokensOnSignout?: boolean;
     revokeTokenTypes?: ("access_token" | "refresh_token")[];
     silent_redirect_uri?: string;
@@ -1024,6 +1027,8 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
     // (undocumented)
     readonly redirectMethod: "replace" | "assign";
     // (undocumented)
+    readonly redirectTarget: "top" | "self";
+    // (undocumented)
     readonly revokeTokensOnSignout: boolean;
     // (undocumented)
     readonly revokeTokenTypes: ("access_token" | "refresh_token")[];
diff --git a/src/UserManagerSettings.ts b/src/UserManagerSettings.ts
index 24f1bdf3..e289474d 100644
--- a/src/UserManagerSettings.ts
+++ b/src/UserManagerSettings.ts
@@ -35,6 +35,8 @@ export interface UserManagerSettings extends OidcClientSettings {
     popupWindowTarget?: string;
     /** The methods window.location method used to redirect (default: "assign") */
     redirectMethod?: "replace" | "assign";
+    /** The methods target window being redirected (default: "self") */
+    redirectTarget?: "top" | "self";
 
     /** The target to pass while calling postMessage inside iframe for callback (default: window.location.origin) */
     iframeNotifyParentOrigin?: string;
@@ -91,6 +93,7 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
     public readonly popupWindowFeatures: PopupWindowFeatures;
     public readonly popupWindowTarget: string;
     public readonly redirectMethod: "replace" | "assign";
+    public readonly redirectTarget: "top" | "self";
 
     public readonly iframeNotifyParentOrigin: string | undefined;
     public readonly iframeScriptOrigin: string | undefined;
@@ -120,6 +123,7 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
             popupWindowFeatures = DefaultPopupWindowFeatures,
             popupWindowTarget = DefaultPopupTarget,
             redirectMethod = "assign",
+            redirectTarget = "self",
 
             iframeNotifyParentOrigin = args.iframeNotifyParentOrigin,
             iframeScriptOrigin = args.iframeScriptOrigin,
@@ -150,6 +154,7 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
         this.popupWindowFeatures = popupWindowFeatures;
         this.popupWindowTarget = popupWindowTarget;
         this.redirectMethod = redirectMethod;
+        this.redirectTarget = redirectTarget;
 
         this.iframeNotifyParentOrigin = iframeNotifyParentOrigin;
         this.iframeScriptOrigin = iframeScriptOrigin;
diff --git a/src/navigators/RedirectNavigator.ts b/src/navigators/RedirectNavigator.ts
index 2c99b695..2c16b9ed 100644
--- a/src/navigators/RedirectNavigator.ts
+++ b/src/navigators/RedirectNavigator.ts
@@ -11,6 +11,7 @@ import type { IWindow } from "./IWindow";
  */
 export interface RedirectParams {
     redirectMethod?: "replace" | "assign";
+    redirectTarget?: "top" | "self";
 }
 
 /**
@@ -23,9 +24,16 @@ export class RedirectNavigator implements INavigator {
 
     public async prepare({
         redirectMethod = this._settings.redirectMethod,
+        redirectTarget = this._settings.redirectTarget,
     }: RedirectParams): Promise<IWindow> {
         this._logger.create("prepare");
-        const redirect = window.location[redirectMethod].bind(window.location) as (url: string) => never;
+        let targetWindow = window.self as Window;
+
+        if (redirectTarget === "top") {
+            targetWindow = window.top ?? window.self;
+        }
+    
+        const redirect = targetWindow.location[redirectMethod].bind(targetWindow.location) as (url: string) => never;
         let abort: (reason: Error) => void;
         return {
             navigate: async (params): Promise<never> => {
@@ -40,7 +48,7 @@ export class RedirectNavigator implements INavigator {
             close: () => {
                 this._logger.create("close");
                 abort?.(new Error("Redirect aborted"));
-                window.stop();
+                targetWindow.stop();
             },
         };
     }


Test patch:
diff --git a/src/navigators/RedirectNavigator.test.ts b/src/navigators/RedirectNavigator.test.ts
index fe84001f..4153f79f 100644
--- a/src/navigators/RedirectNavigator.test.ts
+++ b/src/navigators/RedirectNavigator.test.ts
@@ -31,6 +31,30 @@ describe("RedirectNavigator", () => {
         expect(spy).not.toHaveBeenCalled();
     });
 
+    it("should redirect to the authority server from window top", async () => {
+ 
+        Object.defineProperty(window, "top", {
+            value: {
+                location: {
+                    assign: jest.fn(),
+                },
+            },
+        });
+
+        const handle = await navigator.prepare({ redirectTarget: "top" });
+        const spy = jest.fn();
+        handle.navigate({ url: "http://sts/authorize" }).finally(spy);
+
+        expect(window.location.assign).toHaveBeenCalledTimes(0);
+        expect(window.parent.location.assign).toHaveBeenCalledTimes(0);
+        expect(window.top!.location.assign).toHaveBeenCalledWith("http://sts/authorize");
+
+        // We check that the promise does not resolve even after the window
+        // unload event
+        await once(window, "unload");
+        expect(spy).not.toHaveBeenCalled();
+    });
+
     it("should reject when the navigation is stopped programmatically", async () => {
         const handle = await navigator.prepare({});
         mocked(window.location.assign).mockReturnValue(undefined);