Skip to content

Commit

Permalink
fix: Don't finish the SSO callback before the window is closed (WEBAP…
Browse files Browse the repository at this point in the history
  • Loading branch information
ffflorian committed Jul 2, 2020
1 parent a640b97 commit 8e09238
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 73 deletions.
2 changes: 1 addition & 1 deletion electron/src/sso/SingleSignOn.test.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('SingleSignOn', () => {
describe('generateSecret', () => {
it('generates a secret of a specified size', async () => {
const size = 24;
const loginAuthorizationSecret = await SingleSignOn['protocol'].generateSecret(size);
const loginAuthorizationSecret = await SingleSignOn['generateSecret'](size);
assert.strictEqual(loginAuthorizationSecret.length, size * 2);
});
});
Expand Down
131 changes: 59 additions & 72 deletions electron/src/sso/SingleSignOn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class SingleSignOn {

// Register protocol
// Note: we need to create the window before otherwise it does not work
await SingleSignOn.protocol.register(this.session, type => this.finalizeLogin(type));
await SingleSignOn.registerProtocol(this.session, type => this.finalizeLogin(type));

// Show the window(s)
await this.ssoWindow.loadURL(this.windowOriginUrl.toString());
Expand All @@ -115,7 +115,7 @@ export class SingleSignOn {
}
};

private readonly createBrowserWindow = (): BrowserWindow => {
private createBrowserWindow(): BrowserWindow {
// Discard old preload URL
delete (this.windowOptions as any).webPreferences.preloadURL;
delete (this.windowOptions as any).webPreferences.preload;
Expand Down Expand Up @@ -176,7 +176,7 @@ export class SingleSignOn {
ssoWindow.once('closed', async () => {
if (this.session) {
await this.wipeSessionData();
await SingleSignOn.protocol.unregister(this.session);
await SingleSignOn.unregisterProtocol(this.session);
}
this.session = undefined;
});
Expand Down Expand Up @@ -210,19 +210,11 @@ export class SingleSignOn {
}

return ssoWindow;
};
}

// Ensure authenticity of the window from within the code
public static isSingleSignOnLoginWindow = (frameName: string) => SingleSignOn.SINGLE_SIGN_ON_FRAME_NAME === frameName;

// Ensure the requested URL is going to the backend
public static isBackendOrigin = (url: string): boolean => {
if (url === 'null') {
return false;
}
return SingleSignOn.ALLOWED_BACKEND_ORIGINS.includes(new URL(url).origin);
};

// Returns an empty string if the origin is a Wire backend
public static getWindowTitle = (origin: string): string =>
SingleSignOn.ALLOWED_BACKEND_ORIGINS.includes(origin) ? '' : origin;
Expand Down Expand Up @@ -259,75 +251,70 @@ export class SingleSignOn {
await toSession.cookies.flushStore();
}

private static readonly protocol = {
generateSecret: (length: number): Promise<string> => {
return new Promise((resolve, reject) => {
crypto.randomBytes(length, (error, bytes) => {
if (error) {
return reject(error);
}
resolve(bytes.toString('hex'));
});
});
},
register: async (session: Session, finalizeLogin: (type: string) => void): Promise<void> => {
// Generate a new secret to authenticate the custom protocol (wire-sso)
SingleSignOn.loginAuthorizationSecret = await SingleSignOn.protocol.generateSecret(24);
private static generateSecret(length: number): Promise<string> {
return new Promise((resolve, reject) => {
crypto.randomBytes(length, (error, bytes) => (error ? reject(error) : resolve(bytes.toString('hex'))));
});
}

const handleRequest = (request: ProtocolRequest, response: (data?: string) => void) => {
try {
const requestURL = new URL(request.url);
private static async registerProtocol(session: Session, finalizeLogin: (type: string) => void): Promise<void> {
// Generate a new secret to authenticate the custom protocol (wire-sso)
SingleSignOn.loginAuthorizationSecret = await SingleSignOn.generateSecret(24);

if (requestURL.protocol !== `${SingleSignOn.SSO_PROTOCOL}:`) {
throw new Error('Protocol is invalid');
}
const handleRequest = (request: ProtocolRequest) => {
try {
const requestURL = new URL(request.url);

if (requestURL.hostname !== SingleSignOn.SSO_PROTOCOL_HOST) {
throw new Error('Host is invalid');
}
if (requestURL.protocol !== `${SingleSignOn.SSO_PROTOCOL}:`) {
throw new Error('Protocol is invalid');
}

if (typeof SingleSignOn.loginAuthorizationSecret !== 'string') {
throw new Error('Secret has not be set or has been consumed');
}
if (requestURL.hostname !== SingleSignOn.SSO_PROTOCOL_HOST) {
throw new Error('Host is invalid');
}

if (requestURL.searchParams.get('secret') !== SingleSignOn.loginAuthorizationSecret) {
throw new Error('Secret is invalid');
}
if (typeof SingleSignOn.loginAuthorizationSecret !== 'string') {
throw new Error('Secret has not be set or has been consumed');
}

const type = requestURL.searchParams.get('type');
if (requestURL.searchParams.get('secret') !== SingleSignOn.loginAuthorizationSecret) {
throw new Error('Secret is invalid');
}

if (typeof type !== 'string') {
throw new Error('Response is empty');
}
const type = requestURL.searchParams.get('type');

if (type.length > SingleSignOn.SSO_PROTOCOL_RESPONSE_SIZE_LIMIT) {
throw new Error('Response type is too long');
}
if (typeof type !== 'string') {
throw new Error('Response is empty');
}

finalizeLogin(type);
response('Please wait...');
} catch (error) {
response(`An error happened, please close the window and try again. Error: ${error.toString()}`);
if (type.length > SingleSignOn.SSO_PROTOCOL_RESPONSE_SIZE_LIMIT) {
throw new Error('Response type is too long');
}
};

const isHandled = await session.protocol.isProtocolHandled(SingleSignOn.SSO_PROTOCOL);
if (!isHandled) {
session.protocol.registerStringProtocol(SingleSignOn.SSO_PROTOCOL, handleRequest, error => {
if (error) {
throw new Error(`Failed to register protocol. Error: ${error}`);
}
});
finalizeLogin(type);
} catch (error) {
SingleSignOn.logger.error(error);
}
},
unregister: (session: Session): Promise<void> => {
return new Promise((resolve, reject) => {
session.protocol.unregisterProtocol(SingleSignOn.SSO_PROTOCOL, error => {
return error ? reject(error) : resolve();
});
};

const isHandled = await session.protocol.isProtocolHandled(SingleSignOn.SSO_PROTOCOL);

if (!isHandled) {
session.protocol.registerStringProtocol(SingleSignOn.SSO_PROTOCOL, handleRequest, error => {
if (error) {
throw new Error(`Failed to register protocol. Error: ${error}`);
}
});
},
};
}
}

private static unregisterProtocol(session: Session): Promise<void> {
return new Promise((resolve, reject) => {
session.protocol.unregisterProtocol(SingleSignOn.SSO_PROTOCOL, error => {
return error ? reject(error) : resolve();
});
});
}

private readonly finalizeLogin = async (type: string): Promise<void> => {
if (type === SingleSignOn.RESPONSE_TYPES.AUTH_SUCCESS) {
Expand All @@ -349,7 +336,7 @@ export class SingleSignOn {
await this.dispatchResponse(type);
};

private readonly dispatchResponse = async (type: string): Promise<void> => {
private async dispatchResponse(type: string): Promise<void> {
// Ensure guest window provided type is valid
const isTypeValid = /^[A-Z_]{1,255}$/g;
if (isTypeValid.test(type) === false) {
Expand All @@ -366,11 +353,11 @@ export class SingleSignOn {
this.ssoWindow.close();
this.ssoWindow = undefined;
}
};
}

private readonly wipeSessionData = async () => {
private async wipeSessionData() {
if (this.session) {
await this.session.clearStorageData(undefined);
}
};
}
}

0 comments on commit 8e09238

Please sign in to comment.