Skip to content

Commit

Permalink
feat: silent refresh using different domains
Browse files Browse the repository at this point in the history
- add ability to specify target origin when doing postMessage for parent
- add ability to specify script origin to listen on silent callback
- update unit tests
- update gitignore for local history plugin
  • Loading branch information
pavliy committed May 5, 2022
1 parent ef07b09 commit 96e45b6
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ node_modules/
.DS_Store
.vscode/
temp/
.history/
7 changes: 7 additions & 0 deletions docs/oidc-client-ts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,9 @@ export interface UserManagerSettings extends OidcClientSettings {
accessTokenExpiringNotificationTimeInSeconds?: number;
automaticSilentRenew?: boolean;
checkSessionIntervalInSeconds?: number;
iframeNotifyParentOrigin?: string;
// (undocumented)
iframeScriptOrigin?: string;
includeIdTokenInSilentRenew?: boolean;
// (undocumented)
monitorAnonymousSession?: boolean;
Expand Down Expand Up @@ -1000,6 +1003,10 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
// (undocumented)
readonly checkSessionIntervalInSeconds: number;
// (undocumented)
readonly iframeNotifyParentOrigin: string | undefined;
// (undocumented)
readonly iframeScriptOrigin: string | undefined;
// (undocumented)
readonly includeIdTokenInSilentRenew: boolean;
// (undocumented)
readonly monitorAnonymousSession: boolean;
Expand Down
15 changes: 15 additions & 0 deletions src/UserManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ describe("UserManager", () => {
// act
expect(subject.settings.client_id).toEqual("client");
});

it.each([
{ monitorSession: true, message: "should" },
{ monitorSession: false, message: "should not" },
])("when monitorSession is $monitorSession $message init sessionMonitor", (args) => {
const settings = { ...subject.settings, monitorSession: args.monitorSession };

const userManager = new UserManager(settings);
const sessionMonitor = userManager["_sessionMonitor"];
if (args.monitorSession) {
expect(sessionMonitor).toBeDefined();
} else {
expect(sessionMonitor).toBeNull();
}
});
});

describe("settings", () => {
Expand Down
1 change: 1 addition & 0 deletions src/UserManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ export class UserManager {
url: signinRequest.url,
state: signinRequest.state.id,
response_mode: signinRequest.state.response_mode,
scriptOrigin: this.settings.iframeScriptOrigin,
});
}
catch (err) {
Expand Down
13 changes: 13 additions & 0 deletions src/UserManagerSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export interface UserManagerSettings extends OidcClientSettings {
/** The methods window.location method used to redirect (default: "assign") */
redirectMethod?: "replace" | "assign";

/** The target to pass while calling postMessage inside iframe for callback */
iframeNotifyParentOrigin?: string;
iframeScriptOrigin?: string;

/** The URL for the page containing the code handling the silent renew */
silent_redirect_uri?: string;
/** Number of seconds to wait for the silent renew to return before assuming it has failed or timed out (default: 10) */
Expand Down Expand Up @@ -86,6 +90,9 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
public readonly popupWindowTarget: string;
public readonly redirectMethod: "replace" | "assign";

public readonly iframeNotifyParentOrigin: string | undefined;
public readonly iframeScriptOrigin: string | undefined;

public readonly silent_redirect_uri: string;
public readonly silentRequestTimeoutInSeconds: number;
public readonly automaticSilentRenew: boolean;
Expand All @@ -112,6 +119,9 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
popupWindowTarget = DefaultPopupTarget,
redirectMethod = "assign",

iframeNotifyParentOrigin = args.iframeNotifyParentOrigin,
iframeScriptOrigin = args.iframeScriptOrigin,

silent_redirect_uri = args.redirect_uri,
silentRequestTimeoutInSeconds = DefaultSilentRequestTimeoutInSeconds,
automaticSilentRenew = true,
Expand Down Expand Up @@ -139,6 +149,9 @@ export class UserManagerSettingsStore extends OidcClientSettingsStore {
this.popupWindowTarget = popupWindowTarget;
this.redirectMethod = redirectMethod;

this.iframeNotifyParentOrigin = iframeNotifyParentOrigin;
this.iframeScriptOrigin = iframeScriptOrigin;

this.silent_redirect_uri = silent_redirect_uri;
this.silentRequestTimeoutInSeconds = silentRequestTimeoutInSeconds;
this.automaticSilentRenew = automaticSilentRenew;
Expand Down
7 changes: 4 additions & 3 deletions src/navigators/AbstractChildWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ export abstract class AbstractChildWindow implements IWindow {
const { url, keepOpen } = await new Promise<MessageData>((resolve, reject) => {
const listener = (e: MessageEvent) => {
const data: MessageData | undefined = e.data;
if (e.origin !== window.location.origin || data?.source !== messageSource) {
const origin = params.scriptOrigin ?? window.location.origin;
if (e.origin !== origin || data?.source !== messageSource) {
// silently discard events not intended for us
return;
}
Expand Down Expand Up @@ -86,11 +87,11 @@ export abstract class AbstractChildWindow implements IWindow {
this._disposeHandlers.clear();
}

protected static _notifyParent(parent: Window, url: string, keepOpen = false): void {
protected static _notifyParent(parent: Window, url: string, keepOpen = false, targetOrigin = window.location.origin): void {
parent.postMessage({
source: messageSource,
url,
keepOpen,
} as MessageData, window.location.origin);
} as MessageData, targetOrigin);
}
}
2 changes: 1 addition & 1 deletion src/navigators/IFrameNavigator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ export class IFrameNavigator implements INavigator {

public async callback(url: string): Promise<void> {
this._logger.create("callback");
IFrameWindow.notifyParent(url);
IFrameWindow.notifyParent(url, this._settings.iframeNotifyParentOrigin);
}
}
118 changes: 118 additions & 0 deletions src/navigators/IFrameWindow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { IFrameWindow } from "./IFrameWindow";
import type { NavigateParams } from "./IWindow";

describe("IFrameWindow", () => {
const postMessageMock = jest.fn();
const fakeWindowOrigin = "https://fake-origin.com";
const fakeUrl = "https://fakeurl.com";

afterEach(() => {
jest.clearAllMocks();
});

describe("hidden frame", () => {
let frameWindow: IFrameWindow;

beforeAll(() => {
frameWindow = new IFrameWindow({ });
});

it("should have appropriate styles for hidden presentation", () => {
const { visibility, position, left, top } = frameWindow["_frame"]!.style;

expect(visibility).toBe("hidden");
expect(position).toBe("fixed");
expect(left).toBe("-1000px");
expect(top).toBe("0px");
});

it("should have 0 width and height", () => {
const { width, height } = frameWindow["_frame"]!;
expect(width).toBe("0");
expect(height).toBe("0");
});

it("should set correct sandbox attribute", () => {
const sandboxAttr = frameWindow["_frame"]!.attributes.getNamedItem("sandbox");
expect(sandboxAttr?.value).toBe("allow-scripts allow-same-origin allow-forms");
});
});

describe("close", () => {
let subject: IFrameWindow;
const parentRemoveChild = jest.fn();
beforeEach(() => {
subject = new IFrameWindow({});
jest.spyOn(subject["_frame"]!, "parentNode", "get").mockReturnValue({
removeChild: parentRemoveChild,
} as unknown as ParentNode);
});

it("should reset window to null", () => {
subject.close();
expect(subject["_window"]).toBeNull();
});

describe("if frame defined", () => {
it("should set blank url for contentWindow", () => {
const replaceMock = jest.fn();
jest.spyOn(subject["_frame"]!, "contentWindow", "get")
.mockReturnValue({ location: { replace: replaceMock } } as unknown as WindowProxy);

subject.close();
expect(replaceMock).toBeCalledWith("about:blank");
});

it("should reset frame to null", () => {
subject.close();
expect(subject["_frame"]).toBeNull();
});
});
});

describe("navigate", () => {
const contentWindowMock = jest.fn();

beforeAll(() => {
jest.spyOn(window, "parent", "get").mockReturnValue({
postMessage: postMessageMock,
} as unknown as WindowProxy);
Object.defineProperty(window, "location", {
enumerable: true,
value: { origin: fakeWindowOrigin },
});

contentWindowMock.mockReturnValue(null);
jest.spyOn(window.document.body, "appendChild").mockImplementation();
jest.spyOn(window.document, "createElement").mockReturnValue(({
contentWindow: contentWindowMock(),
style: {},
setAttribute: jest.fn(),
} as unknown as HTMLIFrameElement),
);
});

it("when frame.contentWindow is not defined should throw error", async() => {
const frameWindow = new IFrameWindow({});
await expect(frameWindow.navigate({} as NavigateParams))
.rejects
.toMatchObject({ message: "Attempted to navigate on a disposed window" });
});
});

describe("notifyParent", () => {
const messageData = {
source: "oidc-client",
url: fakeUrl,
keepOpen: false,
};

it.each([
["https://parent-domain.com", "https://parent-domain.com"],
[undefined, fakeWindowOrigin],
])("should call postMessage with appropriate parameters", (targetOrigin, expectedOrigin) => {
IFrameWindow.notifyParent(messageData.url, targetOrigin);
expect(postMessageMock).toBeCalledWith(messageData, expectedOrigin);
});
});
});
4 changes: 2 additions & 2 deletions src/navigators/IFrameWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export class IFrameWindow extends AbstractChildWindow {
this._window = null;
}

public static notifyParent(url: string): void {
return super._notifyParent(window.parent, url);
public static notifyParent(url: string, targetOrigin?: string): void {
return super._notifyParent(window.parent, url, false, targetOrigin);
}
}
1 change: 1 addition & 0 deletions src/navigators/IWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface NavigateParams {
/** The request "state" parameter. For sign out requests, this parameter is optional. */
state?: string;
response_mode?: "query" | "fragment";
scriptOrigin?: string;
}

/**
Expand Down

0 comments on commit 96e45b6

Please sign in to comment.