Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions src/deepLinks/main.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,151 @@ describe('deepLinks/main.ts', () => {
expect(mockWebContents.loadURL).not.toHaveBeenCalled();
});

const runSamlDeepLink = async (deepLink: string): Promise<void> => {
const savedArgv = process.argv;
process.argv = ['electron', '.', deepLink];

await processDeepLinksInArgs();

process.argv = savedArgv;
};
Comment thread
yash-rajpal marked this conversation as resolved.

it('redeems a SAML credentialToken on the target server view', async () => {
setupDeepLinks();

resolveServerUrlMock.mockResolvedValue([
'https://chat.example.com',
ServerUrlResolutionStatus.OK,
undefined,
] as any);
selectMock.mockReturnValue([
{ url: 'https://chat.example.com', title: 'Chat' },
]);

await runSamlDeepLink(
'rocketchat://auth?type=saml&host=https://chat.example.com&credentialToken=saml-fresh-A'
);

expect(resolveServerUrlMock).toHaveBeenCalledWith(
'https://chat.example.com'
);
// The web client's own /saml/:token route redeems the token on this view's connection.
expect(mockWebContents.loadURL).toHaveBeenCalledWith(
'https://chat.example.com/saml/saml-fresh-A'
);
expect(mockWebContents.loadURL).not.toHaveBeenCalledWith(
expect.stringContaining('loginClient')
);
});

it('percent-encodes the SAML credentialToken into the path', async () => {
setupDeepLinks();

resolveServerUrlMock.mockResolvedValue([
'https://chat.example.com',
ServerUrlResolutionStatus.OK,
undefined,
] as any);
selectMock.mockReturnValue([
{ url: 'https://chat.example.com', title: 'Chat' },
]);

await runSamlDeepLink(
'rocketchat://auth?type=saml&host=https://chat.example.com&credentialToken=saml%2Fescape%3FB'
);

// A token cannot break out of the /saml/ path segment and reach another route.
expect(mockWebContents.loadURL).toHaveBeenCalledWith(
'https://chat.example.com/saml/saml%2Fescape%3FB'
);
});

it('does nothing when the SAML deep link has no credentialToken', async () => {
setupDeepLinks();

await runSamlDeepLink(
'rocketchat://auth?type=saml&host=https://chat.example.com'
);

expect(resolveServerUrlMock).not.toHaveBeenCalled();
expect(mockWebContents.loadURL).not.toHaveBeenCalled();
});

it('redeems a different SAML credentialToken after one was consumed', async () => {
setupDeepLinks();

resolveServerUrlMock.mockResolvedValue([
'https://chat.example.com',
ServerUrlResolutionStatus.OK,
undefined,
] as any);
selectMock.mockReturnValue([
{ url: 'https://chat.example.com', title: 'Chat' },
]);

await runSamlDeepLink(
'rocketchat://auth?type=saml&host=https://chat.example.com&credentialToken=saml-first-D'
);
await runSamlDeepLink(
'rocketchat://auth?type=saml&host=https://chat.example.com&credentialToken=saml-second-D'
);

expect(mockWebContents.loadURL).toHaveBeenCalledTimes(2);
expect(mockWebContents.loadURL).toHaveBeenLastCalledWith(
'https://chat.example.com/saml/saml-second-D'
);
});

it('asks to add an unknown server before redeeming a SAML credentialToken', async () => {
setupDeepLinks();

resolveServerUrlMock.mockResolvedValue([
'https://chat.example.com',
ServerUrlResolutionStatus.OK,
undefined,
] as any);
askForServerAdditionMock.mockResolvedValue(true);
selectMock.mockImplementation((selector: any) =>
selector({ servers: [] })
);

await runSamlDeepLink(
'rocketchat://auth?type=saml&host=https://chat.example.com&credentialToken=saml-add-E'
);

expect(askForServerAdditionMock).toHaveBeenCalledWith(
'https://chat.example.com'
);
expect(dispatchMock).toHaveBeenCalledWith({
type: DEEP_LINKS_SERVER_ADDED,
payload: 'https://chat.example.com',
});
expect(mockWebContents.loadURL).toHaveBeenCalledWith(
'https://chat.example.com/saml/saml-add-E'
);
});

it('keeps using the resumeToken path for an unrecognized auth type', async () => {
setupDeepLinks();

resolveServerUrlMock.mockResolvedValue([
'https://chat.example.com',
ServerUrlResolutionStatus.OK,
undefined,
] as any);
selectMock.mockReturnValue([
{ url: 'https://chat.example.com', title: 'Chat' },
]);

await runSamlDeepLink(
'rocketchat://auth?type=whatever&host=https://chat.example.com&token=abc&userId=123'
);

expect(mockWebContents.loadURL).toHaveBeenCalledWith(
'https://chat.example.com/home?resumeToken=abc&userId=123'
);
});

it('processes rocketchat://room link when host and path are valid', async () => {
setupDeepLinks();

Expand Down
45 changes: 39 additions & 6 deletions src/deepLinks/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ type AuthenticationParams = {
userId: string;
};

type SamlAuthenticationParams = {
host: string;
credentialToken: string;
};

type OpenRoomParams = {
host: string;
path?: string;
Expand Down Expand Up @@ -177,6 +182,21 @@ const performAuthentication = async ({
webContents.loadURL(url.href);
});

const performSamlAuthentication = async ({
host,
credentialToken,
}: SamlAuthenticationParams): Promise<void> => {
return performOnServer(host, async (serverUrl) => {
const url = new URL(
`saml/${encodeURIComponent(credentialToken)}`,
serverUrl
);

const webContents = await getWebContents(serverUrl);
webContents.loadURL(url.href);
});
Comment thread
yash-rajpal marked this conversation as resolved.
};

// https://developer.rocket.chat/rocket.chat/deeplink#channel-group-dm
const performOpenRoom = async ({
host,
Expand Down Expand Up @@ -219,6 +239,24 @@ const performConference = async ({ host, path }: InviteParams): Promise<void> =>
webContents.loadURL(new URL(path, serverUrl).href);
});

const performAuthDeepLink = async (args: URLSearchParams): Promise<void> => {
const host = args.get('host') ?? undefined;

if (args.get('type') === 'saml') {
const credentialToken = args.get('credentialToken') ?? undefined;
if (host && credentialToken) {
await performSamlAuthentication({ host, credentialToken });
}
return;
}

const token = args.get('token') ?? undefined;
const userId = args.get('userId') ?? undefined;
if (host && token && userId) {
await performAuthentication({ host, token, userId });
}
};

const processDeepLink = async (deepLink: string): Promise<void> => {
const telephonyLink = parseTelephonyLink(deepLink);
if (telephonyLink) {
Expand All @@ -242,12 +280,7 @@ const processDeepLink = async (deepLink: string): Promise<void> => {

switch (action) {
case 'auth': {
const host = args.get('host') ?? undefined;
const token = args.get('token') ?? undefined;
const userId = args.get('userId') ?? undefined;
if (host && token && userId) {
await performAuthentication({ host, token, userId });
}
await performAuthDeepLink(args);
break;
}

Expand Down
Loading