Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: breaking auth flow on Safari desktop - prevent confirmation popup #618

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 12 additions & 11 deletions packages/auth-client/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,31 +378,32 @@ describe('Auth Client login', () => {
const client = await AuthClient.create();
// Try without #authorize hash.
await client.login({ identityProvider: 'http://localhost' });
expect(global.open).toBeCalledWith('http://localhost/#authorize', 'idpWindow', undefined);
expect(global.open).toBeCalledWith(undefined, 'idpWindow', undefined);
expect((client as unknown as { _idpWindow?: Window })._idpWindow?.location).toEqual(
'http://localhost/#authorize',
);

// Try with #authorize hash.
global.open = jest.fn();
await client.login({ identityProvider: 'http://localhost#authorize' });
expect(global.open).toBeCalledWith('http://localhost/#authorize', 'idpWindow', undefined);
expect(global.open).toBeCalledWith(undefined, 'idpWindow', undefined);
expect((client as unknown as { _idpWindow?: Window })._idpWindow?.location).toEqual(
'http://localhost/#authorize',
);

// Default url
global.open = jest.fn();
await client.login();
expect(global.open).toBeCalledWith(
expect(global.open).toBeCalledWith(undefined, 'idpWindow', undefined);
expect((client as unknown as { _idpWindow?: Window })._idpWindow?.location).toEqual(
'https://identity.ic0.app/#authorize',
'idpWindow',
undefined,
);

// Default custom window.open feature
global.open = jest.fn();
await client.login({
windowOpenerFeatures: 'toolbar=0,location=0,menubar=0',
});
expect(global.open).toBeCalledWith(
expect(global.open).toBeCalledWith(undefined, 'idpWindow', 'toolbar=0,location=0,menubar=0');
expect((client as unknown as { _idpWindow?: Window })._idpWindow?.location).toEqual(
'https://identity.ic0.app/#authorize',
'idpWindow',
'toolbar=0,location=0,menubar=0',
);
});
it('should login with a derivation origin', async () => {
Expand Down
29 changes: 20 additions & 9 deletions packages/auth-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,22 @@ export class AuthClient {
*/
onError?: ((error?: string) => void) | ((error?: string) => Promise<void>);
}): Promise<void> {
// If `login` has been called previously, then close/remove any previous windows
// and event listeners.
this._idpWindow?.close();
this._removeEventListener();

// Open a new window without particular destination.
//
// Context:
// The window needs to be opened first as the browser (Safari) might prevent its opening if an async callback is run before it within the same function.
// i.e. browsers might warn user about new tabs/windows that are open - or detected as - without user interaction. They might require their approval to open a new window ("Popup blocked, do you want to open it?").
// In this particular case, reading the KEY_STORAGE_KEY from the storage is on Safari is detected as an async callback within same function.
//
// While opening a new popup is fundamentally correct, it is an issue in case of II because the parameter "#authorize" - that should be contained in the URL to start the sign in flow - gets lost if the browser request the approval with a prompt.
this._idpWindow =
window.open(undefined, 'idpWindow', options?.windowOpenerFeatures) ?? undefined;

let key = this._key;
if (!key) {
// Create a new key (whether or not one was in storage).
Expand All @@ -396,22 +412,17 @@ export class AuthClient {
// Set the correct hash if it isn't already set.
identityProviderUrl.hash = IDENTITY_PROVIDER_ENDPOINT;

// If `login` has been called previously, then close/remove any previous windows
// and event listeners.
this._idpWindow?.close();
this._removeEventListener();

// Add an event listener to handle responses.
this._eventHandler = this._getEventHandler(identityProviderUrl, {
maxTimeToLive: options?.maxTimeToLive ?? defaultTimeToLive,
...options,
});
window.addEventListener('message', this._eventHandler);

// Open a new window with the IDP provider.
this._idpWindow =
window.open(identityProviderUrl.toString(), 'idpWindow', options?.windowOpenerFeatures) ??
undefined;
// Open to the IDP provider URL in the window that has been opened.
if (this._idpWindow) {
this._idpWindow.location = identityProviderUrl.toString();
}

// Check if the _idpWindow is closed by user.
const checkInterruption = (): void => {
Expand Down