mirror of
https://github.com/Kong/insomnia
synced 2024-11-08 06:39:48 +00:00
Add PKCE support (#2652)
* Add PKCE support * Fix failing unit test and add new test for PKCE * Update packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.js Co-authored-by: Opender Singh <opender94@gmail.com> * Update packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.js Co-authored-by: Opender Singh <opender94@gmail.com> * Fix prettier report Co-authored-by: Opender Singh <opender94@gmail.com>
This commit is contained in:
parent
d201cd1807
commit
294777b0ed
@ -194,4 +194,63 @@ describe('authorization_code', () => {
|
||||
xResponseId: expect.stringMatching(/^res_/),
|
||||
});
|
||||
});
|
||||
|
||||
it('uses PKCE', async () => {
|
||||
createBWRedirectMock(`${REDIRECT_URI}?code=code_123&state=${STATE}`);
|
||||
const bodyPath = path.join(getTempDir(), 'foo.response');
|
||||
|
||||
fs.writeFileSync(
|
||||
bodyPath,
|
||||
JSON.stringify({
|
||||
access_token: 'token_123',
|
||||
token_type: 'token_type',
|
||||
scope: SCOPE,
|
||||
audience: AUDIENCE,
|
||||
resource: RESOURCE,
|
||||
}),
|
||||
);
|
||||
|
||||
network.sendWithSettings = jest.fn(() => ({
|
||||
bodyPath,
|
||||
bodyCompression: '',
|
||||
parentId: 'req_1',
|
||||
statusCode: 200,
|
||||
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
||||
}));
|
||||
|
||||
const result = await getToken(
|
||||
'req_1',
|
||||
AUTHORIZE_URL,
|
||||
ACCESS_TOKEN_URL,
|
||||
false,
|
||||
CLIENT_ID,
|
||||
CLIENT_SECRET,
|
||||
REDIRECT_URI,
|
||||
SCOPE,
|
||||
STATE,
|
||||
AUDIENCE,
|
||||
RESOURCE,
|
||||
true,
|
||||
);
|
||||
|
||||
// Check the request to fetch the token
|
||||
expect(network.sendWithSettings.mock.calls[0][1].body.params).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ name: 'code_verifier' })]),
|
||||
);
|
||||
|
||||
// Check the expected value
|
||||
expect(result).toEqual({
|
||||
access_token: 'token_123',
|
||||
refresh_token: null,
|
||||
expires_in: null,
|
||||
token_type: 'token_type',
|
||||
scope: SCOPE,
|
||||
audience: AUDIENCE,
|
||||
resource: RESOURCE,
|
||||
error: null,
|
||||
error_uri: null,
|
||||
error_description: null,
|
||||
xResponseId: expect.stringMatching(/^res_/),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -15,6 +15,9 @@ export const P_CLIENT_ID = 'client_id';
|
||||
export const P_CLIENT_SECRET = 'client_secret';
|
||||
export const P_AUDIENCE = 'audience';
|
||||
export const P_RESOURCE = 'resource';
|
||||
export const P_CODE_CHALLENGE = 'code_challenge';
|
||||
export const P_CODE_CHALLENGE_METHOD = 'code_challenge_method';
|
||||
export const P_CODE_VERIFIER = 'code_verifier';
|
||||
export const P_CODE = 'code';
|
||||
export const P_NONCE = 'nonce';
|
||||
export const P_ERROR = 'error';
|
||||
|
@ -65,6 +65,7 @@ async function _getOAuth2AuthorizationCodeHeader(
|
||||
authentication.state,
|
||||
authentication.audience,
|
||||
authentication.resource,
|
||||
authentication.usePkce,
|
||||
);
|
||||
|
||||
return _updateOAuth2Token(requestId, results);
|
||||
|
@ -1,4 +1,5 @@
|
||||
// @flow
|
||||
import crypto from 'crypto';
|
||||
import { parse as urlParse } from 'url';
|
||||
import * as c from './constants';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from 'insomnia-url';
|
||||
@ -20,6 +21,7 @@ export default async function(
|
||||
state: string = '',
|
||||
audience: string = '',
|
||||
resource: string = '',
|
||||
usePkce: boolean = false,
|
||||
): Promise<Object> {
|
||||
if (!authorizeUrl) {
|
||||
throw new Error('Invalid authorization URL');
|
||||
@ -29,6 +31,19 @@ export default async function(
|
||||
throw new Error('Invalid access token URL');
|
||||
}
|
||||
|
||||
let codeVerifier = '';
|
||||
let codeChallenge = '';
|
||||
|
||||
if (usePkce) {
|
||||
codeVerifier = _base64UrlEncode(crypto.randomBytes(32));
|
||||
codeChallenge = _base64UrlEncode(
|
||||
crypto
|
||||
.createHash('sha256')
|
||||
.update(codeVerifier)
|
||||
.digest(),
|
||||
);
|
||||
}
|
||||
|
||||
const authorizeResults = await _authorize(
|
||||
authorizeUrl,
|
||||
clientId,
|
||||
@ -37,6 +52,7 @@ export default async function(
|
||||
state,
|
||||
audience,
|
||||
resource,
|
||||
codeChallenge,
|
||||
);
|
||||
|
||||
// Handle the error
|
||||
@ -58,6 +74,7 @@ export default async function(
|
||||
state,
|
||||
audience,
|
||||
resource,
|
||||
codeVerifier,
|
||||
);
|
||||
}
|
||||
|
||||
@ -69,6 +86,7 @@ async function _authorize(
|
||||
state = '',
|
||||
audience = '',
|
||||
resource = '',
|
||||
codeChallenge = '',
|
||||
) {
|
||||
const params = [
|
||||
{ name: c.P_RESPONSE_TYPE, value: c.RESPONSE_TYPE_CODE },
|
||||
@ -82,6 +100,11 @@ async function _authorize(
|
||||
audience && params.push({ name: c.P_AUDIENCE, value: audience });
|
||||
resource && params.push({ name: c.P_RESOURCE, value: resource });
|
||||
|
||||
if (codeChallenge) {
|
||||
params.push({ name: c.P_CODE_CHALLENGE, value: codeChallenge });
|
||||
params.push({ name: c.P_CODE_CHALLENGE_METHOD, value: 'S256' });
|
||||
}
|
||||
|
||||
// Add query params to URL
|
||||
const qs = buildQueryStringFromParams(params);
|
||||
const finalUrl = joinUrlAndQueryString(url, qs);
|
||||
@ -113,6 +136,7 @@ async function _getToken(
|
||||
state: string = '',
|
||||
audience: string = '',
|
||||
resource: string = '',
|
||||
codeVerifier: string = '',
|
||||
): Promise<Object> {
|
||||
const params = [
|
||||
{ name: c.P_GRANT_TYPE, value: c.GRANT_TYPE_AUTHORIZATION_CODE },
|
||||
@ -124,6 +148,7 @@ async function _getToken(
|
||||
state && params.push({ name: c.P_STATE, value: state });
|
||||
audience && params.push({ name: c.P_AUDIENCE, value: audience });
|
||||
resource && params.push({ name: c.P_RESOURCE, value: resource });
|
||||
codeVerifier && params.push({ name: c.P_CODE_VERIFIER, value: codeVerifier });
|
||||
|
||||
const headers = [
|
||||
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
|
||||
@ -182,3 +207,15 @@ async function _getToken(
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function _base64UrlEncode(str: string): string {
|
||||
return str
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
|
||||
// The characters + / = are reserved for PKCE as per the RFC,
|
||||
// so we replace them with unreserved characters
|
||||
// Docs: https://tools.ietf.org/html/rfc7636#section-4.2
|
||||
}
|
||||
|
@ -161,6 +161,10 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
|
||||
this._handleChangeProperty('clientSecret', value);
|
||||
}
|
||||
|
||||
_handleChangePkce(value: boolean): void {
|
||||
this._handleChangeProperty('usePkce', value);
|
||||
}
|
||||
|
||||
_handleChangeAuthorizationUrl(value: string): void {
|
||||
this._handleChangeProperty('authorizationUrl', value);
|
||||
}
|
||||
@ -235,6 +239,36 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
renderUsePkceRow(onChange: boolean => void): React.Element<*> {
|
||||
const { request } = this.props;
|
||||
const { authentication } = request;
|
||||
return (
|
||||
<tr key="use-pkce">
|
||||
<td className="pad-right no-wrap valign-middle">
|
||||
<label htmlFor="use-pkce" className="label--small no-pad">
|
||||
Use PKCE
|
||||
</label>
|
||||
</td>
|
||||
<td className="wide">
|
||||
<div className="form-control form-control--underlined no-margin">
|
||||
<Button
|
||||
className="btn btn--super-duper-compact"
|
||||
id="use-pkce"
|
||||
onClick={onChange}
|
||||
value={authentication.usePkce}
|
||||
title={authentication.usePkce ? 'Disable PKCE' : 'Enable PKCE'}>
|
||||
{authentication.usePkce ? (
|
||||
<i className="fa fa-check-square-o" />
|
||||
) : (
|
||||
<i className="fa fa-square-o" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
renderInputRow(
|
||||
label: string,
|
||||
property: string,
|
||||
@ -336,6 +370,8 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
|
||||
this._handleChangeClientSecret,
|
||||
);
|
||||
|
||||
const usePkce = this.renderUsePkceRow(this._handleChangePkce);
|
||||
|
||||
const authorizationUrl = this.renderInputRow(
|
||||
'Authorization URL',
|
||||
'authorizationUrl',
|
||||
@ -422,6 +458,7 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
usePkce,
|
||||
redirectUri,
|
||||
enabled,
|
||||
];
|
||||
|
Loading…
Reference in New Issue
Block a user