Skip to content

Commit

Permalink
Add option to disable state verification (#1339)
Browse files Browse the repository at this point in the history
* Add option to disable state verification
  • Loading branch information
srajiang committed Dec 10, 2021
1 parent 92c8af0 commit 1e06865
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 45 deletions.
6 changes: 5 additions & 1 deletion packages/oauth/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export enum ErrorCode {
AuthorizationError = 'slack_oauth_installer_authorization_error',
GenerateInstallUrlError = 'slack_oauth_generate_url_error',
MissingStateError = 'slack_oauth_missing_state',
MissingCodeError = 'slack_oauth_missing_code',
UnknownError = 'slack_oauth_unknown_error',
}

Expand All @@ -20,11 +21,14 @@ export class InstallerInitializationError extends Error implements CodedError {
export class GenerateInstallUrlError extends Error implements CodedError {
public code = ErrorCode.GenerateInstallUrlError;
}

export class MissingStateError extends Error implements CodedError {
public code = ErrorCode.MissingStateError;
}

export class MissingCodeError extends Error implements CodedError {
public code = ErrorCode.MissingCodeError;
}

export class UnknownError extends Error implements CodedError {
public code = ErrorCode.UnknownError;
}
Expand Down
107 changes: 88 additions & 19 deletions packages/oauth/src/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ describe('OAuth', async () => {
const installer = new InstallProvider({ clientId, clientSecret });
} catch (error) {
assert.equal(error.code, ErrorCode.InstallerInitializationError);
assert.equal(error.message, 'You must provide a State Secret to use the built-in state store');
assert.equal(error.message, 'To use the built-in state store you must provide a State Secret');
}
});
});
Expand All @@ -246,16 +246,17 @@ describe('OAuth', async () => {
const scopes = ['channels:read'];
const teamId = '1234Team';
const redirectUri = 'https://mysite.com/slack/redirect';
const userScopes = ['chat:write:user']
const userScopes = ['chat:write:user'];
const stateVerification = true;
const installUrlOptions = {
scopes,
metadata: 'some_metadata',
teamId,
redirectUri,
userScopes,
}
};
try {
const generatedUrl = await installer.generateInstallUrl(installUrlOptions)
const generatedUrl = await installer.generateInstallUrl(installUrlOptions, stateVerification)
assert.exists(generatedUrl);
assert.equal(fakeStateStore.generateStateParam.callCount, 1);
assert.equal(fakeStateStore.verifyStateParam.callCount, 0);
Expand All @@ -272,8 +273,7 @@ describe('OAuth', async () => {
assert.fail(error.message);
}
});

it('should return a generated url when passed a custom authorizationUrl', async () => {
it('should not call generate state param when state validation is false', async () => {
const fakeStateStore = {
generateStateParam: sinon.fake.resolves('fakeState'),
verifyStateParam: sinon.fake.resolves({})
Expand All @@ -284,13 +284,42 @@ describe('OAuth', async () => {
const teamId = '1234Team';
const redirectUri = 'https://mysite.com/slack/redirect';
const userScopes = ['chat:write:user']
const stateVerification = false;
const installUrlOptions = {
scopes,
metadata: 'some_metadata',
teamId,
redirectUri,
userScopes,
};
try {
const generatedUrl = await installer.generateInstallUrl(installUrlOptions, stateVerification)
assert.exists(generatedUrl);
assert.equal(fakeStateStore.generateStateParam.callCount, 0);
assert.equal(fakeStateStore.verifyStateParam.callCount, 0);
} catch (error) {
assert.fail(error.message);
}
});
it('should return a generated url when passed a custom authorizationUrl', async () => {
const fakeStateStore = {
generateStateParam: sinon.fake.resolves('fakeState'),
verifyStateParam: sinon.fake.resolves({})
}
const authorizationUrl = 'https://dev.slack.com/oauth/v2/authorize';
const installer = new InstallProvider({ clientId, clientSecret, stateStore: fakeStateStore, authorizationUrl });
const scopes = ['channels:read'];
const teamId = '1234Team';
const redirectUri = 'https://mysite.com/slack/redirect';
const userScopes = ['chat:write:user']
const stateVerification = true;
const installUrlOptions = {
scopes,
metadata: 'some_metadata',
teamId,
redirectUri,
userScopes,
};
try {
const generatedUrl = await installer.generateInstallUrl(installUrlOptions)
assert.exists(generatedUrl);
Expand Down Expand Up @@ -320,14 +349,15 @@ describe('OAuth', async () => {
const scopes = ['bot'];
const teamId = '1234Team';
const redirectUri = 'https://mysite.com/slack/redirect';
const stateVerification = true;
const installUrlOptions = {
scopes,
metadata: 'some_metadata',
teamId,
redirectUri,
}
};
try {
const generatedUrl = await installer.generateInstallUrl(installUrlOptions)
const generatedUrl = await installer.generateInstallUrl(installUrlOptions, stateVerification)
assert.exists(generatedUrl);
const parsedUrl = url.parse(generatedUrl, true);
assert.equal(fakeStateStore.generateStateParam.callCount, 1);
Expand Down Expand Up @@ -396,9 +426,8 @@ describe('OAuth', async () => {
verifyStateParam: sinon.fake.resolves({})
}
});

it('should call the failure callback due to missing state query parameter on the URL', async () => {
const req = { url: 'http://example.com' };
it('should call the failure callback due to missing code query parameter on the URL', async () => {
const req = { headers: { host: 'example.com'}, url: 'http://example.com' };
let sent = false;
const res = { send: () => { sent = true; } };
const callbackOptions = {
Expand All @@ -407,7 +436,7 @@ describe('OAuth', async () => {
assert.fail('should have failed');
},
failure: async (error, installOptions, req, res) => {
assert.equal(error.code, ErrorCode.MissingStateError)
assert.equal(error.code, ErrorCode.MissingCodeError)
res.send('failure');
},
}
Expand All @@ -416,9 +445,8 @@ describe('OAuth', async () => {

assert.isTrue(sent);
});

it('should call the failure callback due to missing code query parameter on the URL', async () => {
const req = { url: 'http://example.com' };
it('should call the failure callback due to missing state query parameter on the URL', async () => {
const req = { headers: { host: 'example.com'}, url: 'http://example.com?code=1234' };
let sent = false;
const res = { send: () => { sent = true; } };
const callbackOptions = {
Expand All @@ -437,8 +465,26 @@ describe('OAuth', async () => {
assert.isTrue(sent);
});

it('should call the success callback when state query param is missing but stateVerification disabled', async () => {
const req = { headers: { host: 'example.com'}, url: 'http://example.com?code=1234' };
let sent = false;
const res = { send: () => { sent = true; } };
const callbackOptions = {
success: async (installation, installOptions, req, res) => {
res.send('successful!');
},
failure: async (error, installOptions, req, res) => {
assert.fail('should have succeeded');
},
}
const installer = new InstallProvider({ clientId, clientSecret, stateSecret, stateVerification: false, installationStore, logger: noopLogger });
await installer.handleCallback(req, res, callbackOptions);

assert.isTrue(sent);
});

it('should call the failure callback if an access_denied error query parameter was returned on the URL', async () => {
const req = { url: 'http://example.com?error=access_denied' };
const req = { headers: { host: 'example.com'}, url: 'http://example.com?error=access_denied' };
let sent = false;
const res = { send: () => { sent = true; } };
const callbackOptions = {
Expand Down Expand Up @@ -466,14 +512,13 @@ describe('OAuth', async () => {
},
failure: async (error, installOptions, req, res) => {
assert.fail(error.message);
res.send('failure');
},
}

const installer = new InstallProvider({ clientId, clientSecret, installationStore, stateStore: fakeStateStore });
const fakeState = 'fakeState';
const fakeCode = 'fakeCode';
const req = { url: `http://example.com?state=${fakeState}&code=${fakeCode}` };
const req = { headers: { host: 'example.com'}, url: `http://example.com?state=${fakeState}&code=${fakeCode}` };
await installer.handleCallback(req, res, callbackOptions);
assert.isTrue(sent);
assert.equal(fakeStateStore.verifyStateParam.callCount, 1);
Expand All @@ -494,11 +539,35 @@ describe('OAuth', async () => {
const installer = new InstallProvider({ clientId, clientSecret, stateSecret, installationStore, stateStore: fakeStateStore, authVersion: 'v1' });
const fakeState = 'fakeState';
const fakeCode = 'fakeCode';
const req = { url: `http://example.com?state=${fakeState}&code=${fakeCode}` };
const req = { headers: { host: 'example.com'}, url: `http://example.com?state=${fakeState}&code=${fakeCode}` };
await installer.handleCallback(req, res, callbackOptions);
assert.isTrue(sent);
assert.equal(fakeStateStore.verifyStateParam.callCount, 1);
});
it('should not verify state when stateVerification is false', async () => {
const fakeStateStore = {
generateStateParam: sinon.fake.resolves('fakeState'),
verifyStateParam: sinon.fake.resolves({})
};
let sent = false;
const res = { send: () => { sent = true; } };
const callbackOptions = {
success: async (installation, installOptions, req, res) => {
res.send('successful!');
},
failure: async (error, installOptions, req, res) => {
res.send('failure');
assert.fail('should have sent!');
},
};
const installer = new InstallProvider({ clientId, clientSecret, stateSecret, stateVerification: false, installationStore, stateStore: fakeStateStore, });
const fakeState = 'fakeState';
const fakeCode = 'fakeCode';
const req = { headers: { host: 'example.com'}, url: `http://example.com?state=${fakeState}&code=${fakeCode}` };
await installer.handleCallback(req, res, callbackOptions);
assert.isTrue(sent);
assert.equal(fakeStateStore.verifyStateParam.callCount, 0);
});
});

describe('MemoryInstallationStore', async () => {
Expand Down
Loading

0 comments on commit 1e06865

Please sign in to comment.