mirror of
https://github.com/Kong/insomnia
synced 2024-11-07 22:30:15 +00:00
Refactor/oauth2 (#5571)
* get-token * named export * another pass * exporting const breaks types in a good way * flatten optional params * remove silly db functions * remove junk function in db models * flatten grant auth code * simplify responseToObject * improve response to object typing * use union type for auth keys * remove authkey constants * some hacking * another pass * unpacking * obscure auth object * fix auth code types * fix grant implicit types * remove oauth2 token from redux store * fix oauth method args * extract network from grant auth code * extract send from grant password * extract grant implicit * extract client creds * flatten param generator * extract nuller * flatten refresh token * flatten refresh token * fix types * remove tests * fix test flake * fix gql body test import * fix typos * fix grant auth * fix token clear * fix token state * fix auth header logic * fix types * fix * simplification pass * fix code verifier * flatten implicit grant type * fix typo * use consistent url parsers * flatten grant auth code * fix typo * type oauth2 * improve helper and tidy * fix type * tidy * tidy * tidy up * remove some constants
This commit is contained in:
parent
d86330445e
commit
c4d2939e7a
@ -4,7 +4,6 @@ import { Cookie as ToughCookie } from 'tough-cookie';
|
||||
|
||||
import * as models from '../models';
|
||||
import type { Request } from '../models/request';
|
||||
import { newBodyRaw } from '../models/request';
|
||||
import type { Response } from '../models/response';
|
||||
import { isWorkspace } from '../models/workspace';
|
||||
import { getAuthHeader } from '../network/authentication';
|
||||
@ -423,7 +422,7 @@ function getResponseCookies(response: Response) {
|
||||
|
||||
try {
|
||||
cookie = ToughCookie.parse(harCookie.value || '');
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
|
||||
if (cookie === null || cookie === undefined) {
|
||||
return accumulator;
|
||||
@ -520,10 +519,11 @@ function getRequestQueryString(renderedRequest: RenderedRequest): HarQueryString
|
||||
|
||||
function getRequestPostData(renderedRequest: RenderedRequest): HarPostData | undefined {
|
||||
let body;
|
||||
|
||||
if (renderedRequest.body.fileName) {
|
||||
try {
|
||||
body = newBodyRaw(fs.readFileSync(renderedRequest.body.fileName, 'base64'));
|
||||
body = {
|
||||
text: fs.readFileSync(renderedRequest.body.fileName, 'base64'),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('[code gen] Failed to read file', error);
|
||||
return;
|
||||
|
@ -2,8 +2,8 @@ import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||
|
||||
import { globalBeforeEach } from '../../__jest__/before-each';
|
||||
import { CONTENT_TYPE_GRAPHQL } from '../../common/constants';
|
||||
import { newBodyGraphQL, updateMimeType } from '../../ui/components/panes/request-pane';
|
||||
import * as models from '../index';
|
||||
import { newBodyGraphQL } from '../request';
|
||||
|
||||
describe('init()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
@ -87,7 +87,7 @@ describe('updateMimeType()', () => {
|
||||
parentId: 'fld_1',
|
||||
});
|
||||
expect(request).not.toBeNull();
|
||||
const newRequest = await models.request.updateMimeType(request, 'text/html');
|
||||
const newRequest = await updateMimeType(request, 'text/html');
|
||||
expect(newRequest.headers).toEqual([
|
||||
{
|
||||
name: 'Content-Type',
|
||||
@ -116,7 +116,7 @@ describe('updateMimeType()', () => {
|
||||
],
|
||||
});
|
||||
expect(request).not.toBeNull();
|
||||
const newRequest = await models.request.updateMimeType(request, 'text/html');
|
||||
const newRequest = await updateMimeType(request, 'text/html');
|
||||
expect(newRequest.headers).toEqual([
|
||||
{
|
||||
name: 'content-tYPE',
|
||||
@ -145,7 +145,7 @@ describe('updateMimeType()', () => {
|
||||
],
|
||||
});
|
||||
expect(request).not.toBeNull();
|
||||
const newRequest = await models.request.updateMimeType(request, 'text/html');
|
||||
const newRequest = await updateMimeType(request, 'text/html');
|
||||
expect(newRequest.headers).toEqual([
|
||||
{
|
||||
name: 'content-tYPE',
|
||||
@ -166,7 +166,7 @@ describe('updateMimeType()', () => {
|
||||
],
|
||||
});
|
||||
expect(request).not.toBeNull();
|
||||
const newRequest = await models.request.updateMimeType(request, null);
|
||||
const newRequest = await updateMimeType(request, null);
|
||||
expect(newRequest.body).toEqual({});
|
||||
expect(newRequest.headers).toEqual([]);
|
||||
});
|
||||
@ -180,7 +180,7 @@ describe('updateMimeType()', () => {
|
||||
},
|
||||
});
|
||||
expect(request).not.toBeNull();
|
||||
const newRequest = await models.request.updateMimeType(request, 'application/json', false, {
|
||||
const newRequest = await updateMimeType(request, 'application/json', false, {
|
||||
text: 'Saved Data',
|
||||
});
|
||||
expect(newRequest.body.text).toEqual('Saved Data');
|
||||
@ -195,7 +195,7 @@ describe('updateMimeType()', () => {
|
||||
},
|
||||
});
|
||||
expect(request).not.toBeNull();
|
||||
const newRequest = await models.request.updateMimeType(request, 'application/json', false, {});
|
||||
const newRequest = await updateMimeType(request, 'application/json', false, {});
|
||||
expect(newRequest.body.text).toEqual('My Data');
|
||||
});
|
||||
});
|
||||
|
@ -9,19 +9,12 @@ import {
|
||||
AUTH_NTLM,
|
||||
AUTH_OAUTH_1,
|
||||
AUTH_OAUTH_2,
|
||||
CONTENT_TYPE_FILE,
|
||||
CONTENT_TYPE_FORM_DATA,
|
||||
CONTENT_TYPE_FORM_URLENCODED,
|
||||
CONTENT_TYPE_GRAPHQL,
|
||||
CONTENT_TYPE_JSON,
|
||||
CONTENT_TYPE_OTHER,
|
||||
getContentTypeFromHeaders,
|
||||
HAWK_ALGORITHM_SHA256,
|
||||
METHOD_GET,
|
||||
METHOD_POST,
|
||||
} from '../common/constants';
|
||||
import { database as db } from '../common/database';
|
||||
import { getContentTypeHeader } from '../common/misc';
|
||||
import { SIGNATURE_METHOD_HMAC_SHA1 } from '../network/o-auth-1/constants';
|
||||
import { GRANT_TYPE_AUTHORIZATION_CODE } from '../network/o-auth-2/constants';
|
||||
import { deconstructQueryStringToParams } from '../utils/url/querystring';
|
||||
@ -38,7 +31,30 @@ export const canDuplicate = true;
|
||||
export const canSync = true;
|
||||
|
||||
export type RequestAuthentication = Record<string, any>;
|
||||
|
||||
export interface AuthTypeOAuth2 {
|
||||
type: 'oauth2';
|
||||
grantType: 'authorization_code' | 'client_credentials' | 'password' | 'implicit' | 'refresh_token';
|
||||
accessTokenUrl?: string;
|
||||
authorizationUrl?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
audience?: string;
|
||||
scope?: string;
|
||||
resource?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
redirectUrl?: string;
|
||||
credentialsInBody?: boolean;
|
||||
state?: string;
|
||||
code?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
tokenPrefix?: string;
|
||||
usePkce?: boolean;
|
||||
pkceMethod?: string;
|
||||
responseType?: string;
|
||||
origin?: string;
|
||||
}
|
||||
export interface RequestHeader {
|
||||
name: string;
|
||||
value: string;
|
||||
@ -206,65 +222,6 @@ export function newAuth(type: string, oldAuth: RequestAuthentication = {}): Requ
|
||||
}
|
||||
}
|
||||
|
||||
export function newBodyNone(): RequestBody {
|
||||
return {};
|
||||
}
|
||||
|
||||
export function newBodyRaw(rawBody: string, contentType?: string): RequestBody {
|
||||
if (typeof contentType !== 'string') {
|
||||
return {
|
||||
text: rawBody,
|
||||
};
|
||||
}
|
||||
|
||||
const mimeType = contentType.split(';')[0];
|
||||
return {
|
||||
mimeType,
|
||||
text: rawBody,
|
||||
};
|
||||
}
|
||||
|
||||
export function newBodyGraphQL(rawBody: string): RequestBody {
|
||||
try {
|
||||
// Only strip the newlines if rawBody is a parsable JSON
|
||||
JSON.parse(rawBody);
|
||||
return {
|
||||
mimeType: CONTENT_TYPE_GRAPHQL,
|
||||
text: rawBody.replace(/\\\\n/g, ''),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
return {
|
||||
mimeType: CONTENT_TYPE_GRAPHQL,
|
||||
text: rawBody,
|
||||
};
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function newBodyFormUrlEncoded(parameters: RequestBodyParameter[] | null): RequestBody {
|
||||
return {
|
||||
mimeType: CONTENT_TYPE_FORM_URLENCODED,
|
||||
params: parameters || [],
|
||||
};
|
||||
}
|
||||
|
||||
export function newBodyFile(path: string): RequestBody {
|
||||
return {
|
||||
mimeType: CONTENT_TYPE_FILE,
|
||||
fileName: path,
|
||||
};
|
||||
}
|
||||
|
||||
export function newBodyForm(parameters: RequestBodyParameter[]): RequestBody {
|
||||
return {
|
||||
mimeType: CONTENT_TYPE_FORM_DATA,
|
||||
params: parameters || [],
|
||||
};
|
||||
}
|
||||
|
||||
export function migrate(doc: Request): Request {
|
||||
doc = migrateBody(doc);
|
||||
doc = migrateWeirdUrls(doc);
|
||||
@ -292,105 +249,6 @@ export function update(request: Request, patch: Partial<Request>) {
|
||||
return db.docUpdate<Request>(request, patch);
|
||||
}
|
||||
|
||||
export function updateMimeType(
|
||||
request: Request,
|
||||
mimeType: string,
|
||||
doCreate = false,
|
||||
savedBody: RequestBody = {},
|
||||
) {
|
||||
let headers = request.headers ? [...request.headers] : [];
|
||||
const contentTypeHeader = getContentTypeHeader(headers);
|
||||
// GraphQL uses JSON content-type
|
||||
const contentTypeHeaderValue = mimeType === CONTENT_TYPE_GRAPHQL ? CONTENT_TYPE_JSON : mimeType;
|
||||
|
||||
// GraphQL must be POST
|
||||
if (mimeType === CONTENT_TYPE_GRAPHQL) {
|
||||
request.method = METHOD_POST;
|
||||
}
|
||||
|
||||
// Check if we are converting to/from variants of XML or JSON
|
||||
let leaveContentTypeAlone = false;
|
||||
|
||||
if (contentTypeHeader && mimeType) {
|
||||
const current = contentTypeHeader.value;
|
||||
|
||||
if (current.includes('xml') && mimeType.includes('xml')) {
|
||||
leaveContentTypeAlone = true;
|
||||
} else if (current.includes('json') && mimeType.includes('json')) {
|
||||
leaveContentTypeAlone = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// 1. Update Content-Type header //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
const hasBody = typeof mimeType === 'string';
|
||||
|
||||
if (!hasBody) {
|
||||
headers = headers.filter(h => h !== contentTypeHeader);
|
||||
} else if (mimeType === CONTENT_TYPE_OTHER) {
|
||||
// Leave headers alone
|
||||
} else if (mimeType && contentTypeHeader && !leaveContentTypeAlone) {
|
||||
contentTypeHeader.value = contentTypeHeaderValue;
|
||||
} else if (mimeType && !contentTypeHeader) {
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: contentTypeHeaderValue,
|
||||
});
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// 2. Make a new request body //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
let body;
|
||||
const oldBody = Object.keys(savedBody).length === 0 ? request.body : savedBody;
|
||||
|
||||
if (mimeType === CONTENT_TYPE_FORM_URLENCODED) {
|
||||
// Urlencoded
|
||||
body = oldBody.params
|
||||
? newBodyFormUrlEncoded(oldBody.params)
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
: newBodyFormUrlEncoded(deconstructQueryStringToParams(oldBody.text));
|
||||
} else if (mimeType === CONTENT_TYPE_FORM_DATA) {
|
||||
// Form Data
|
||||
body = oldBody.params
|
||||
? newBodyForm(oldBody.params)
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
: newBodyForm(deconstructQueryStringToParams(oldBody.text));
|
||||
} else if (mimeType === CONTENT_TYPE_FILE) {
|
||||
// File
|
||||
body = newBodyFile('');
|
||||
} else if (mimeType === CONTENT_TYPE_GRAPHQL) {
|
||||
if (contentTypeHeader) {
|
||||
contentTypeHeader.value = CONTENT_TYPE_JSON;
|
||||
}
|
||||
|
||||
body = newBodyGraphQL(oldBody.text || '');
|
||||
} else if (typeof mimeType !== 'string') {
|
||||
// No body
|
||||
body = newBodyNone();
|
||||
} else {
|
||||
// Raw Content-Type (ex: application/json)
|
||||
body = newBodyRaw(oldBody.text || '', mimeType);
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// 2. create/update request //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
if (doCreate) {
|
||||
const newRequest: Request = Object.assign({}, request, {
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
return create(newRequest);
|
||||
} else {
|
||||
return update(request, {
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function duplicate(request: Request, patch: Partial<Request> = {}) {
|
||||
// Only set name and "(Copy)" if the patch does
|
||||
// not define it and the request itself has a name.
|
||||
@ -450,13 +308,20 @@ function migrateBody(request: Request) {
|
||||
|
||||
if (wasFormUrlEncoded) {
|
||||
// Convert old-style form-encoded request bodies to new style
|
||||
const body = typeof request.body === 'string' ? request.body : '';
|
||||
request.body = newBodyFormUrlEncoded(deconstructQueryStringToParams(body, false));
|
||||
request.body = {
|
||||
mimeType: CONTENT_TYPE_FORM_URLENCODED,
|
||||
params: deconstructQueryStringToParams(typeof request.body === 'string' ? request.body : '', false),
|
||||
};
|
||||
} else if (!request.body && !contentType) {
|
||||
request.body = {};
|
||||
} else {
|
||||
const body: string = typeof request.body === 'string' ? request.body : '';
|
||||
request.body = newBodyRaw(body, contentType);
|
||||
const rawBody: string = typeof request.body === 'string' ? request.body : '';
|
||||
request.body = typeof contentType !== 'string' ? {
|
||||
text: rawBody,
|
||||
} : {
|
||||
mimeType: contentType.split(';')[0],
|
||||
text: rawBody,
|
||||
};
|
||||
}
|
||||
|
||||
return request;
|
||||
|
@ -157,7 +157,7 @@ export async function getLatestForRequest(
|
||||
return response || null;
|
||||
}
|
||||
|
||||
export async function create(patch: Record<string, any> = {}, maxResponses = 20) {
|
||||
export async function create(patch: Partial<Response> = {}, maxResponses = 20): Promise<Response> {
|
||||
if (!patch.parentId) {
|
||||
throw new Error('New Response missing `parentId`');
|
||||
}
|
||||
@ -168,17 +168,12 @@ export async function create(patch: Record<string, any> = {}, maxResponses = 20)
|
||||
const requestVersion = request ? await models.requestVersion.create(request) : null;
|
||||
patch.requestVersionId = requestVersion ? requestVersion._id : null;
|
||||
// Filter responses by environment if setting is enabled
|
||||
const query: Record<string, any> = {
|
||||
const shouldQueryByEnvId = (await models.settings.getOrCreate()).filterResponsesByEnv && patch.hasOwnProperty('environmentId');
|
||||
const query = {
|
||||
parentId,
|
||||
...(shouldQueryByEnvId ? { environmentId: patch.environmentId } : {}),
|
||||
};
|
||||
|
||||
if (
|
||||
(await models.settings.getOrCreate()).filterResponsesByEnv &&
|
||||
patch.hasOwnProperty('environmentId')
|
||||
) {
|
||||
query.environmentId = patch.environmentId;
|
||||
}
|
||||
|
||||
// Delete all other responses before creating the new one
|
||||
const allResponses = await db.findMostRecentlyModified<Response>(type, query, Math.max(1, maxResponses));
|
||||
const recentIds = allResponses.map(r => r._id);
|
||||
|
@ -11,12 +11,12 @@ import {
|
||||
AUTH_OAUTH_2,
|
||||
} from '../common/constants';
|
||||
import type { RenderedRequest } from '../common/render';
|
||||
import { RequestAuthentication, RequestParameter } from '../models/request';
|
||||
import { AuthTypeOAuth2, RequestAuthentication, RequestParameter } from '../models/request';
|
||||
import { COOKIE, HEADER, QUERY_PARAMS } from './api-key/constants';
|
||||
import { getBasicAuthHeader } from './basic-auth/get-header';
|
||||
import { getBearerAuthHeader } from './bearer-auth/get-header';
|
||||
import getOAuth1Token from './o-auth-1/get-token';
|
||||
import getOAuth2Token from './o-auth-2/get-token';
|
||||
import { getOAuth2Token } from './o-auth-2/get-token';
|
||||
|
||||
interface Header {
|
||||
name: string;
|
||||
@ -64,7 +64,7 @@ export async function getAuthHeader(renderedRequest: RenderedRequest, url: strin
|
||||
// pretending we are fetching a token for the original request. This makes sure
|
||||
// the same tokens are used for schema fetching. See issue #835 on GitHub.
|
||||
const tokenId = requestId.match(/\.graphql$/) ? requestId.replace(/\.graphql$/, '') : requestId;
|
||||
const oAuth2Token = await getOAuth2Token(tokenId, authentication);
|
||||
const oAuth2Token = await getOAuth2Token(tokenId, authentication as AuthTypeOAuth2);
|
||||
|
||||
if (oAuth2Token) {
|
||||
const token = oAuth2Token.accessToken;
|
||||
|
@ -1,307 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import { getTempDir } from '../../../common/electron-helpers';
|
||||
import * as network from '../../network';
|
||||
import getToken from '../grant-authorization-code';
|
||||
|
||||
// Mock some test things
|
||||
const AUTHORIZE_URL = 'https://foo.com/authorizeAuthCode';
|
||||
const ACCESS_TOKEN_URL = 'https://foo.com/access_token';
|
||||
const CLIENT_ID = 'client_123';
|
||||
const CLIENT_SECRET = 'secret_12345456677756343';
|
||||
const REDIRECT_URI = 'https://foo.com/redirect';
|
||||
const SCOPE = 'scope_123';
|
||||
const STATE = 'state_123';
|
||||
const AUDIENCE = 'https://foo.com/resource';
|
||||
const RESOURCE = 'foo.com';
|
||||
|
||||
describe('authorization_code', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('gets token with JSON and basic auth', async () => {
|
||||
window.main = { authorizeUserInWindow: () => Promise.resolve(`${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,
|
||||
);
|
||||
// Check the request to fetch the token
|
||||
expect(network.sendWithSettings.mock.calls).toEqual([
|
||||
[
|
||||
'req_1',
|
||||
{
|
||||
url: ACCESS_TOKEN_URL,
|
||||
method: 'POST',
|
||||
body: {
|
||||
mimeType: 'application/x-www-form-urlencoded',
|
||||
params: [
|
||||
{
|
||||
name: 'grant_type',
|
||||
value: 'authorization_code',
|
||||
},
|
||||
{
|
||||
name: 'code',
|
||||
value: 'code_123',
|
||||
},
|
||||
{
|
||||
name: 'redirect_uri',
|
||||
value: REDIRECT_URI,
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
value: STATE,
|
||||
},
|
||||
{
|
||||
name: 'audience',
|
||||
value: AUDIENCE,
|
||||
},
|
||||
{
|
||||
name: 'resource',
|
||||
value: RESOURCE,
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
},
|
||||
{
|
||||
name: 'Authorization',
|
||||
value: 'Basic Y2xpZW50XzEyMzpzZWNyZXRfMTIzNDU0NTY2Nzc3NTYzNDM=',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
// Check the expected value
|
||||
expect(result).toEqual({
|
||||
access_token: 'token_123',
|
||||
id_token: null,
|
||||
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_/),
|
||||
});
|
||||
});
|
||||
|
||||
it('gets token with urlencoded and body auth', async () => {
|
||||
window.main = { authorizeUserInWindow: () => Promise.resolve(`${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/x-www-form-urlencoded',
|
||||
},
|
||||
],
|
||||
}));
|
||||
const result = await getToken(
|
||||
'req_1',
|
||||
AUTHORIZE_URL,
|
||||
ACCESS_TOKEN_URL,
|
||||
true,
|
||||
CLIENT_ID,
|
||||
CLIENT_SECRET,
|
||||
REDIRECT_URI,
|
||||
SCOPE,
|
||||
STATE,
|
||||
AUDIENCE,
|
||||
RESOURCE,
|
||||
);
|
||||
// Check the request to fetch the token
|
||||
expect(network.sendWithSettings.mock.calls).toEqual([
|
||||
[
|
||||
'req_1',
|
||||
{
|
||||
url: ACCESS_TOKEN_URL,
|
||||
method: 'POST',
|
||||
body: {
|
||||
mimeType: 'application/x-www-form-urlencoded',
|
||||
params: [
|
||||
{
|
||||
name: 'grant_type',
|
||||
value: 'authorization_code',
|
||||
},
|
||||
{
|
||||
name: 'code',
|
||||
value: 'code_123',
|
||||
},
|
||||
{
|
||||
name: 'redirect_uri',
|
||||
value: REDIRECT_URI,
|
||||
},
|
||||
{
|
||||
name: 'state',
|
||||
value: STATE,
|
||||
},
|
||||
{
|
||||
name: 'audience',
|
||||
value: AUDIENCE,
|
||||
},
|
||||
{
|
||||
name: 'resource',
|
||||
value: RESOURCE,
|
||||
},
|
||||
{
|
||||
name: 'client_id',
|
||||
value: CLIENT_ID,
|
||||
},
|
||||
{
|
||||
name: 'client_secret',
|
||||
value: CLIENT_SECRET,
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
// Check the expected value
|
||||
expect(result).toEqual({
|
||||
access_token: 'token_123',
|
||||
id_token: null,
|
||||
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_/),
|
||||
});
|
||||
});
|
||||
|
||||
it('uses PKCE', async () => {
|
||||
window.main = { authorizeUserInWindow: () => Promise.resolve(`${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',
|
||||
id_token: null,
|
||||
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_/),
|
||||
});
|
||||
});
|
||||
});
|
@ -1,218 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import { getTempDir } from '../../../common/electron-helpers';
|
||||
import * as network from '../../network';
|
||||
import getToken from '../grant-client-credentials';
|
||||
|
||||
// Mock some test things
|
||||
const ACCESS_TOKEN_URL = 'https://foo.com/access_token';
|
||||
const CLIENT_ID = 'client_123';
|
||||
const CLIENT_SECRET = 'secret_12345456677756343';
|
||||
const SCOPE = 'scope_123';
|
||||
const AUDIENCE = 'https://foo.com/userinfo';
|
||||
const RESOURCE = 'https://foo.com/resource';
|
||||
|
||||
describe('client_credentials', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('gets token with JSON and basic auth', async () => {
|
||||
const bodyPath = path.join(getTempDir(), 'foo.response');
|
||||
fs.writeFileSync(
|
||||
bodyPath,
|
||||
JSON.stringify({
|
||||
access_token: 'token_123',
|
||||
refresh_token: 'token_456',
|
||||
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',
|
||||
ACCESS_TOKEN_URL,
|
||||
false,
|
||||
CLIENT_ID,
|
||||
CLIENT_SECRET,
|
||||
SCOPE,
|
||||
AUDIENCE,
|
||||
RESOURCE,
|
||||
);
|
||||
// Check the request to fetch the token
|
||||
expect(network.sendWithSettings.mock.calls).toEqual([
|
||||
[
|
||||
'req_1',
|
||||
{
|
||||
url: ACCESS_TOKEN_URL,
|
||||
method: 'POST',
|
||||
body: {
|
||||
mimeType: 'application/x-www-form-urlencoded',
|
||||
params: [
|
||||
{
|
||||
name: 'grant_type',
|
||||
value: 'client_credentials',
|
||||
},
|
||||
{
|
||||
name: 'scope',
|
||||
value: SCOPE,
|
||||
},
|
||||
{
|
||||
name: 'audience',
|
||||
value: AUDIENCE,
|
||||
},
|
||||
{
|
||||
name: 'resource',
|
||||
value: RESOURCE,
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
},
|
||||
{
|
||||
name: 'Authorization',
|
||||
value: 'Basic Y2xpZW50XzEyMzpzZWNyZXRfMTIzNDU0NTY2Nzc3NTYzNDM=',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
// Check the expected value
|
||||
expect(result).toEqual({
|
||||
access_token: 'token_123',
|
||||
id_token: null,
|
||||
refresh_token: 'token_456',
|
||||
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_/),
|
||||
});
|
||||
});
|
||||
|
||||
it('gets token with urlencoded and body auth', async () => {
|
||||
const bodyPath = path.join(getTempDir(), 'req_1.response');
|
||||
fs.writeFileSync(
|
||||
bodyPath,
|
||||
JSON.stringify({
|
||||
access_token: 'token_123',
|
||||
refresh_token: 'token_456',
|
||||
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/x-www-form-urlencoded',
|
||||
},
|
||||
],
|
||||
}));
|
||||
const result = await getToken(
|
||||
'req_1',
|
||||
ACCESS_TOKEN_URL,
|
||||
true,
|
||||
CLIENT_ID,
|
||||
CLIENT_SECRET,
|
||||
SCOPE,
|
||||
AUDIENCE,
|
||||
RESOURCE,
|
||||
);
|
||||
// Check the request to fetch the token
|
||||
expect(network.sendWithSettings.mock.calls).toEqual([
|
||||
[
|
||||
'req_1',
|
||||
{
|
||||
url: ACCESS_TOKEN_URL,
|
||||
method: 'POST',
|
||||
body: {
|
||||
mimeType: 'application/x-www-form-urlencoded',
|
||||
params: [
|
||||
{
|
||||
name: 'grant_type',
|
||||
value: 'client_credentials',
|
||||
},
|
||||
{
|
||||
name: 'scope',
|
||||
value: SCOPE,
|
||||
},
|
||||
{
|
||||
name: 'audience',
|
||||
value: AUDIENCE,
|
||||
},
|
||||
{
|
||||
name: 'resource',
|
||||
value: RESOURCE,
|
||||
},
|
||||
{
|
||||
name: 'client_id',
|
||||
value: CLIENT_ID,
|
||||
},
|
||||
{
|
||||
name: 'client_secret',
|
||||
value: CLIENT_SECRET,
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
// Check the expected value
|
||||
expect(result).toEqual({
|
||||
access_token: 'token_123',
|
||||
id_token: null,
|
||||
refresh_token: 'token_456',
|
||||
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_/),
|
||||
});
|
||||
});
|
||||
});
|
@ -1,33 +0,0 @@
|
||||
import { beforeEach, describe, expect, it } from '@jest/globals';
|
||||
|
||||
import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import getToken from '../grant-implicit';
|
||||
|
||||
// Mock some test things
|
||||
const AUTHORIZE_URL = 'https://foo.com/authorizeAuthCode';
|
||||
const CLIENT_ID = 'client_123';
|
||||
const REDIRECT_URI = 'https://foo.com/redirect';
|
||||
const AUDIENCE = 'https://foo.com/userinfo';
|
||||
const SCOPE = 'scope_123';
|
||||
const STATE = 'state_123';
|
||||
|
||||
describe('implicit', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('works in default case', async () => {
|
||||
window.main = { authorizeUserInWindow: () => Promise.resolve(`${REDIRECT_URI}#access_token=token_123&state=${STATE}&foo=bar`) };
|
||||
|
||||
const result = await getToken(AUTHORIZE_URL, CLIENT_ID, REDIRECT_URI, SCOPE, STATE, AUDIENCE);
|
||||
expect(result).toEqual({
|
||||
access_token: 'token_123',
|
||||
id_token: null,
|
||||
token_type: null,
|
||||
expires_in: null,
|
||||
scope: null,
|
||||
state: STATE,
|
||||
error: null,
|
||||
error_description: null,
|
||||
error_uri: null,
|
||||
});
|
||||
});
|
||||
});
|
@ -1,223 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import { getTempDir } from '../../../common/electron-helpers';
|
||||
import * as network from '../../network';
|
||||
import getToken from '../grant-password';
|
||||
|
||||
// Mock some test things
|
||||
const ACCESS_TOKEN_URL = 'https://foo.com/access_token';
|
||||
const CLIENT_ID = 'client_123';
|
||||
const CLIENT_SECRET = 'secret_12345456677756343';
|
||||
const USERNAME = 'user';
|
||||
const PASSWORD = 'password';
|
||||
const SCOPE = 'scope_123';
|
||||
const AUDIENCE = 'https://foo.com/userinfo';
|
||||
|
||||
describe('password', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('gets token with JSON and basic auth', async () => {
|
||||
const bodyPath = path.join(getTempDir(), 'foo.response');
|
||||
fs.writeFileSync(
|
||||
bodyPath,
|
||||
JSON.stringify({
|
||||
access_token: 'token_123',
|
||||
token_type: 'token_type',
|
||||
scope: SCOPE,
|
||||
audience: AUDIENCE,
|
||||
}),
|
||||
);
|
||||
network.sendWithSettings = jest.fn(() => ({
|
||||
bodyPath,
|
||||
bodyCompression: '',
|
||||
parentId: 'req_1',
|
||||
statusCode: 200,
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
},
|
||||
],
|
||||
}));
|
||||
const result = await getToken(
|
||||
'req_1',
|
||||
ACCESS_TOKEN_URL,
|
||||
false,
|
||||
CLIENT_ID,
|
||||
CLIENT_SECRET,
|
||||
USERNAME,
|
||||
PASSWORD,
|
||||
SCOPE,
|
||||
AUDIENCE,
|
||||
);
|
||||
// Check the request to fetch the token
|
||||
expect(network.sendWithSettings.mock.calls).toEqual([
|
||||
[
|
||||
'req_1',
|
||||
{
|
||||
url: ACCESS_TOKEN_URL,
|
||||
method: 'POST',
|
||||
body: {
|
||||
mimeType: 'application/x-www-form-urlencoded',
|
||||
params: [
|
||||
{
|
||||
name: 'grant_type',
|
||||
value: 'password',
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
value: USERNAME,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
value: PASSWORD,
|
||||
},
|
||||
{
|
||||
name: 'scope',
|
||||
value: SCOPE,
|
||||
},
|
||||
{
|
||||
name: 'audience',
|
||||
value: AUDIENCE,
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
},
|
||||
{
|
||||
name: 'Authorization',
|
||||
value: 'Basic Y2xpZW50XzEyMzpzZWNyZXRfMTIzNDU0NTY2Nzc3NTYzNDM=',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
// Check the expected value
|
||||
expect(result).toEqual({
|
||||
access_token: 'token_123',
|
||||
id_token: null,
|
||||
expires_in: null,
|
||||
token_type: 'token_type',
|
||||
refresh_token: null,
|
||||
scope: SCOPE,
|
||||
audience: AUDIENCE,
|
||||
error: null,
|
||||
error_uri: null,
|
||||
error_description: null,
|
||||
xResponseId: expect.stringMatching(/^res_/),
|
||||
});
|
||||
});
|
||||
|
||||
it('gets token with urlencoded and body auth', async () => {
|
||||
const bodyPath = path.join(getTempDir(), 'foo.response');
|
||||
fs.writeFileSync(
|
||||
bodyPath,
|
||||
JSON.stringify({
|
||||
access_token: 'token_123',
|
||||
token_type: 'token_type',
|
||||
scope: SCOPE,
|
||||
audience: AUDIENCE,
|
||||
}),
|
||||
);
|
||||
network.sendWithSettings = jest.fn(() => ({
|
||||
bodyPath,
|
||||
bodyCompression: '',
|
||||
parentId: 'req_1',
|
||||
statusCode: 200,
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
],
|
||||
}));
|
||||
const result = await getToken(
|
||||
'req_1',
|
||||
ACCESS_TOKEN_URL,
|
||||
true,
|
||||
CLIENT_ID,
|
||||
CLIENT_SECRET,
|
||||
USERNAME,
|
||||
PASSWORD,
|
||||
SCOPE,
|
||||
AUDIENCE,
|
||||
);
|
||||
// Check the request to fetch the token
|
||||
expect(network.sendWithSettings.mock.calls).toEqual([
|
||||
[
|
||||
'req_1',
|
||||
{
|
||||
url: ACCESS_TOKEN_URL,
|
||||
method: 'POST',
|
||||
body: {
|
||||
mimeType: 'application/x-www-form-urlencoded',
|
||||
params: [
|
||||
{
|
||||
name: 'grant_type',
|
||||
value: 'password',
|
||||
},
|
||||
{
|
||||
name: 'username',
|
||||
value: USERNAME,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
value: PASSWORD,
|
||||
},
|
||||
{
|
||||
name: 'scope',
|
||||
value: SCOPE,
|
||||
},
|
||||
{
|
||||
name: 'audience',
|
||||
value: AUDIENCE,
|
||||
},
|
||||
{
|
||||
name: 'client_id',
|
||||
value: CLIENT_ID,
|
||||
},
|
||||
{
|
||||
name: 'client_secret',
|
||||
value: CLIENT_SECRET,
|
||||
},
|
||||
],
|
||||
},
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
// Check the expected value
|
||||
expect(result).toEqual({
|
||||
access_token: 'token_123',
|
||||
id_token: null,
|
||||
expires_in: null,
|
||||
token_type: 'token_type',
|
||||
refresh_token: null,
|
||||
scope: SCOPE,
|
||||
audience: AUDIENCE,
|
||||
error: null,
|
||||
error_uri: null,
|
||||
error_description: null,
|
||||
xResponseId: expect.stringMatching(/^res_/),
|
||||
});
|
||||
});
|
||||
});
|
@ -3,63 +3,11 @@ import { mocked } from 'jest-mock';
|
||||
|
||||
import { globalBeforeEach } from '../../../__jest__/before-each';
|
||||
import * as models from '../../../models';
|
||||
import { authorizeUserInWindow, ChromiumVerificationResult, responseToObject } from '../misc';
|
||||
import { authorizeUserInWindow, ChromiumVerificationResult } from '../misc';
|
||||
import { createBWRedirectMock } from './helpers';
|
||||
|
||||
const MOCK_AUTHORIZATION_URL = 'https://foo.com';
|
||||
|
||||
describe('responseToObject()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
it('works in the general case', () => {
|
||||
const body = JSON.stringify({
|
||||
str: 'hi',
|
||||
num: 10,
|
||||
});
|
||||
const keys = ['str', 'num'];
|
||||
expect(responseToObject(body, keys)).toEqual({
|
||||
str: 'hi',
|
||||
num: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('skips things not in keys', () => {
|
||||
const body = JSON.stringify({
|
||||
str: 'hi',
|
||||
num: 10,
|
||||
other: 'thing',
|
||||
});
|
||||
const keys = ['str'];
|
||||
expect(responseToObject(body, keys)).toEqual({
|
||||
str: 'hi',
|
||||
});
|
||||
});
|
||||
|
||||
it('works with things not found', () => {
|
||||
const body = JSON.stringify({});
|
||||
const keys = ['str'];
|
||||
expect(responseToObject(body, keys)).toEqual({
|
||||
str: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('works with default values', () => {
|
||||
const body = JSON.stringify({
|
||||
str: 'hi',
|
||||
num: 10,
|
||||
});
|
||||
const keys = ['str', 'missing'];
|
||||
const defaults = {
|
||||
missing: 'found it!',
|
||||
str: 'should not see this',
|
||||
};
|
||||
expect(responseToObject(body, keys, defaults)).toEqual({
|
||||
str: 'hi',
|
||||
missing: 'found it!',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('authorizeUserInWindow()', () => {
|
||||
beforeEach(globalBeforeEach);
|
||||
|
||||
|
@ -7,31 +7,32 @@ export const RESPONSE_TYPE_CODE = 'code';
|
||||
export const RESPONSE_TYPE_ID_TOKEN = 'id_token';
|
||||
export const RESPONSE_TYPE_TOKEN = 'token';
|
||||
export const RESPONSE_TYPE_ID_TOKEN_TOKEN = 'id_token token';
|
||||
export const P_ACCESS_TOKEN = 'access_token';
|
||||
export const P_ID_TOKEN = 'id_token';
|
||||
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';
|
||||
export const P_ERROR_DESCRIPTION = 'error_description';
|
||||
export const P_ERROR_URI = 'error_uri';
|
||||
export const P_EXPIRES_IN = 'expires_in';
|
||||
export const P_GRANT_TYPE = 'grant_type';
|
||||
export const P_PASSWORD = 'password';
|
||||
export const P_REDIRECT_URI = 'redirect_uri';
|
||||
export const P_REFRESH_TOKEN = 'refresh_token';
|
||||
export const P_RESPONSE_TYPE = 'response_type';
|
||||
export const P_SCOPE = 'scope';
|
||||
export const P_STATE = 'state';
|
||||
export const P_TOKEN_TYPE = 'token_type';
|
||||
export const P_USERNAME = 'username';
|
||||
export const X_RESPONSE_ID = 'xResponseId';
|
||||
export const X_ERROR = 'xError';
|
||||
export type AuthKeys =
|
||||
'access_token' |
|
||||
'id_token' |
|
||||
'client_id' |
|
||||
'client_secret' |
|
||||
'audience' |
|
||||
'resource' |
|
||||
'code_challenge' |
|
||||
'code_challenge_method' |
|
||||
'code_verifier' |
|
||||
'code' |
|
||||
'nonce' |
|
||||
'error' |
|
||||
'error_description' |
|
||||
'error_uri' |
|
||||
'expires_in' |
|
||||
'grant_type' |
|
||||
'password' |
|
||||
'redirect_uri' |
|
||||
'refresh_token' |
|
||||
'response_type' |
|
||||
'scope' |
|
||||
'state' |
|
||||
'token_type' |
|
||||
'username' |
|
||||
'xError' |
|
||||
'xResponseId';
|
||||
export const PKCE_CHALLENGE_S256 = 'S256';
|
||||
export const PKCE_CHALLENGE_PLAIN = 'plain';
|
||||
|
@ -1,229 +1,306 @@
|
||||
import crypto from 'crypto';
|
||||
import querystring from 'querystring';
|
||||
|
||||
import { escapeRegex } from '../../common/misc';
|
||||
import * as models from '../../models';
|
||||
import type { OAuth2Token } from '../../models/o-auth-2-token';
|
||||
import type { RequestAuthentication } from '../../models/request';
|
||||
import type { AuthTypeOAuth2, RequestHeader, RequestParameter } from '../../models/request';
|
||||
import type { Response } from '../../models/response';
|
||||
import { invariant } from '../../utils/invariant';
|
||||
import { setDefaultProtocol } from '../../utils/url/protocol';
|
||||
import { getBasicAuthHeader } from '../basic-auth/get-header';
|
||||
import { sendWithSettings } from '../network';
|
||||
import {
|
||||
AuthKeys,
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
GRANT_TYPE_CLIENT_CREDENTIALS,
|
||||
GRANT_TYPE_IMPLICIT,
|
||||
GRANT_TYPE_PASSWORD,
|
||||
P_ACCESS_TOKEN,
|
||||
P_ERROR,
|
||||
P_ERROR_DESCRIPTION,
|
||||
P_ERROR_URI,
|
||||
P_EXPIRES_IN,
|
||||
P_ID_TOKEN,
|
||||
P_REFRESH_TOKEN,
|
||||
X_ERROR,
|
||||
X_RESPONSE_ID,
|
||||
PKCE_CHALLENGE_S256,
|
||||
RESPONSE_TYPE_CODE,
|
||||
RESPONSE_TYPE_ID_TOKEN,
|
||||
RESPONSE_TYPE_ID_TOKEN_TOKEN,
|
||||
} from './constants';
|
||||
import getAccessTokenAuthorizationCode from './grant-authorization-code';
|
||||
import getAccessTokenClientCredentials from './grant-client-credentials';
|
||||
import getAccessTokenImplicit from './grant-implicit';
|
||||
import getAccessTokenPassword from './grant-password';
|
||||
import refreshAccessToken from './refresh-token';
|
||||
/** Get an OAuth2Token object and also handle storing/saving/refreshing */
|
||||
import { getOAuthSession } from './misc';
|
||||
|
||||
export default async function(
|
||||
// NOTE
|
||||
// 1. return valid access token from insomnia db
|
||||
// 2. send refresh token in order to save and return valid access token
|
||||
// 3. run a given grant type and save and return valid access token
|
||||
export const getOAuth2Token = async (
|
||||
requestId: string,
|
||||
authentication: RequestAuthentication,
|
||||
authentication: AuthTypeOAuth2,
|
||||
forceRefresh = false,
|
||||
): Promise<OAuth2Token | null> {
|
||||
switch (authentication.grantType) {
|
||||
case GRANT_TYPE_AUTHORIZATION_CODE:
|
||||
return _getOAuth2AuthorizationCodeHeader(requestId, authentication, forceRefresh);
|
||||
|
||||
case GRANT_TYPE_CLIENT_CREDENTIALS:
|
||||
return _getOAuth2ClientCredentialsHeader(requestId, authentication, forceRefresh);
|
||||
|
||||
case GRANT_TYPE_IMPLICIT:
|
||||
return _getOAuth2ImplicitHeader(requestId, authentication, forceRefresh);
|
||||
|
||||
case GRANT_TYPE_PASSWORD:
|
||||
return _getOAuth2PasswordHeader(requestId, authentication, forceRefresh);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function _getOAuth2AuthorizationCodeHeader(
|
||||
requestId: string,
|
||||
authentication: RequestAuthentication,
|
||||
forceRefresh: boolean,
|
||||
): Promise<OAuth2Token | null> {
|
||||
const oAuth2Token = await _getAccessToken(requestId, authentication, forceRefresh);
|
||||
|
||||
): Promise<OAuth2Token | null> => {
|
||||
const oAuth2Token = await getExisingAccessTokenAndRefreshIfExpired(requestId, authentication, forceRefresh);
|
||||
if (oAuth2Token) {
|
||||
return oAuth2Token;
|
||||
}
|
||||
const validGrantType = ['implicit', 'authorization_code', 'password', 'client_credentials'].includes(authentication.grantType);
|
||||
invariant(validGrantType, `Invalid grant type ${authentication.grantType}`);
|
||||
if (authentication.grantType === 'implicit') {
|
||||
invariant(authentication.authorizationUrl, 'Missing authorization URL');
|
||||
const hasNonce = !authentication.responseType || authentication.responseType === RESPONSE_TYPE_ID_TOKEN_TOKEN || authentication.responseType === RESPONSE_TYPE_ID_TOKEN;
|
||||
const implicitUrl = new URL(authentication.authorizationUrl);
|
||||
[
|
||||
{ name: 'response_type', value: authentication.responseType },
|
||||
{ name: 'client_id', value: authentication.clientId },
|
||||
...insertAuthKeyIf('redirect_uri', authentication.redirectUrl),
|
||||
...insertAuthKeyIf('scope', authentication.scope),
|
||||
...insertAuthKeyIf('state', authentication.state),
|
||||
...insertAuthKeyIf('audience', authentication.audience),
|
||||
...(hasNonce ? [{
|
||||
name: 'nonce', value: Math.floor(Math.random() * 9999999999999) + 1 + '',
|
||||
}] : []),
|
||||
].forEach(p => p.value && implicitUrl.searchParams.append(p.name, p.value));
|
||||
const redirectedTo = await window.main.authorizeUserInWindow({
|
||||
url: implicitUrl.toString(),
|
||||
urlSuccessRegex: /(access_token=|id_token=)/,
|
||||
urlFailureRegex: /(error=)/,
|
||||
sessionId: getOAuthSession(),
|
||||
});
|
||||
console.log('[oauth2] Detected redirect ' + redirectedTo);
|
||||
|
||||
const results = await getAccessTokenAuthorizationCode(
|
||||
requestId,
|
||||
authentication.authorizationUrl,
|
||||
authentication.accessTokenUrl,
|
||||
authentication.credentialsInBody,
|
||||
authentication.clientId,
|
||||
authentication.clientSecret,
|
||||
authentication.redirectUrl,
|
||||
authentication.scope,
|
||||
authentication.state,
|
||||
authentication.audience,
|
||||
authentication.resource,
|
||||
authentication.usePkce,
|
||||
authentication.pkceMethod,
|
||||
authentication.origin,
|
||||
);
|
||||
return _updateOAuth2Token(requestId, results);
|
||||
}
|
||||
const hash = new URL(redirectedTo).hash.slice(1);
|
||||
invariant(hash, 'No hash found in redirect URL');
|
||||
const data = Object.fromEntries(new URLSearchParams(hash));
|
||||
const old = await models.oAuth2Token.getOrCreateByParentId(requestId);
|
||||
return models.oAuth2Token.update(old, transformNewAccessTokenToOauthModel({
|
||||
...data,
|
||||
access_token: data.access_token || data.id_token,
|
||||
}));
|
||||
}
|
||||
invariant(authentication.accessTokenUrl, 'Missing access token URL');
|
||||
let params: RequestHeader[] = [];
|
||||
if (authentication.grantType === 'authorization_code') {
|
||||
invariant(authentication.redirectUrl, 'Missing redirect URL');
|
||||
invariant(authentication.authorizationUrl, 'Invalid authorization URL');
|
||||
|
||||
async function _getOAuth2ClientCredentialsHeader(
|
||||
requestId: string,
|
||||
authentication: RequestAuthentication,
|
||||
forceRefresh: boolean,
|
||||
): Promise<OAuth2Token | null> {
|
||||
const oAuth2Token = await _getAccessToken(requestId, authentication, forceRefresh);
|
||||
|
||||
if (oAuth2Token) {
|
||||
return oAuth2Token;
|
||||
const codeVerifier = authentication.usePkce ? encodePKCE(crypto.randomBytes(32)) : '';
|
||||
const codeChallenge = authentication.pkceMethod !== PKCE_CHALLENGE_S256 ? codeVerifier : encodePKCE(crypto.createHash('sha256').update(codeVerifier).digest());
|
||||
const authCodeUrl = new URL(authentication.authorizationUrl);
|
||||
[
|
||||
{ name: 'response_type', value: RESPONSE_TYPE_CODE },
|
||||
{ name: 'client_id', value: authentication.clientId },
|
||||
...insertAuthKeyIf('redirect_uri', authentication.redirectUrl),
|
||||
...insertAuthKeyIf('scope', authentication.scope),
|
||||
...insertAuthKeyIf('state', authentication.state),
|
||||
...insertAuthKeyIf('audience', authentication.audience),
|
||||
...insertAuthKeyIf('resource', authentication.resource),
|
||||
...(codeChallenge ? [
|
||||
{ name: 'code_challenge', value: codeChallenge },
|
||||
{ name: 'code_challenge_method', value: authentication.pkceMethod },
|
||||
] : []),
|
||||
].forEach(p => p.value && authCodeUrl.searchParams.append(p.name, p.value));
|
||||
const redirectedTo = await window.main.authorizeUserInWindow({
|
||||
url: authCodeUrl.toString(),
|
||||
urlSuccessRegex: new RegExp(`${escapeRegex(authentication.redirectUrl)}.*(code=)`, 'i'),
|
||||
urlFailureRegex: new RegExp(`${escapeRegex(authentication.redirectUrl)}.*(error=)`, 'i'),
|
||||
sessionId: getOAuthSession(),
|
||||
});
|
||||
console.log('[oauth2] Detected redirect ' + redirectedTo);
|
||||
const redirectParams = Object.fromEntries(new URL(redirectedTo).searchParams);
|
||||
if (redirectParams.error) {
|
||||
const code = redirectParams.error;
|
||||
const msg = redirectParams.error_description;
|
||||
const uri = redirectParams.error_uri;
|
||||
throw new Error(`OAuth 2.0 Error ${code}\n\n${msg}\n\n${uri}`);
|
||||
}
|
||||
params = [
|
||||
{ name: 'grant_type', value: GRANT_TYPE_AUTHORIZATION_CODE },
|
||||
{ name: 'code', value: redirectParams.code },
|
||||
...insertAuthKeyIf('redirect_uri', authentication.redirectUrl),
|
||||
...insertAuthKeyIf('state', authentication.state),
|
||||
...insertAuthKeyIf('audience', authentication.audience),
|
||||
...insertAuthKeyIf('resource', authentication.resource),
|
||||
...insertAuthKeyIf('code_verifier', codeVerifier),
|
||||
];
|
||||
} else if (authentication.grantType === 'password') {
|
||||
params = [
|
||||
{ name: 'grant_type', value: 'password' },
|
||||
...insertAuthKeyIf('username', authentication.username),
|
||||
...insertAuthKeyIf('password', authentication.password),
|
||||
...insertAuthKeyIf('scope', authentication.scope),
|
||||
...insertAuthKeyIf('audience', authentication.audience),
|
||||
];
|
||||
} else if (authentication.grantType === 'client_credentials') {
|
||||
params = [
|
||||
{ name: 'grant_type', value: 'client_credentials' },
|
||||
...insertAuthKeyIf('scope', authentication.scope),
|
||||
...insertAuthKeyIf('audience', authentication.audience),
|
||||
...insertAuthKeyIf('resource', authentication.resource),
|
||||
];
|
||||
}
|
||||
const headers = authentication.origin ? [{ name: 'Origin', value: authentication.origin }] : [];
|
||||
if (authentication.credentialsInBody) {
|
||||
params = [
|
||||
...params,
|
||||
...insertAuthKeyIf('client_id', authentication.clientId),
|
||||
...insertAuthKeyIf('client_secret', authentication.clientSecret),
|
||||
];
|
||||
} else {
|
||||
headers.push(getBasicAuthHeader(authentication.clientId, authentication.clientSecret));
|
||||
}
|
||||
|
||||
const results = await getAccessTokenClientCredentials(
|
||||
requestId,
|
||||
authentication.accessTokenUrl,
|
||||
authentication.credentialsInBody,
|
||||
authentication.clientId,
|
||||
authentication.clientSecret,
|
||||
authentication.scope,
|
||||
authentication.audience,
|
||||
authentication.resource,
|
||||
);
|
||||
return _updateOAuth2Token(requestId, results);
|
||||
}
|
||||
const response = await sendAccessTokenRequest(requestId, authentication, params, headers);
|
||||
const old = await models.oAuth2Token.getOrCreateByParentId(requestId);
|
||||
return models.oAuth2Token.update(old, transformNewAccessTokenToOauthModel(
|
||||
oauthResponseToAccessToken(authentication.accessTokenUrl, response)
|
||||
));
|
||||
};
|
||||
// 1. get token from db and return if valid
|
||||
// 2. if expired, and no refresh token return null
|
||||
// 3. run refresh token query and return new token or null if it fails
|
||||
|
||||
async function _getOAuth2ImplicitHeader(
|
||||
async function getExisingAccessTokenAndRefreshIfExpired(
|
||||
requestId: string,
|
||||
authentication: RequestAuthentication,
|
||||
authentication: AuthTypeOAuth2,
|
||||
forceRefresh: boolean,
|
||||
): Promise<OAuth2Token | null> {
|
||||
const oAuth2Token = await _getAccessToken(requestId, authentication, forceRefresh);
|
||||
|
||||
if (oAuth2Token) {
|
||||
return oAuth2Token;
|
||||
}
|
||||
|
||||
const results = await getAccessTokenImplicit(
|
||||
requestId,
|
||||
authentication.authorizationUrl,
|
||||
authentication.clientId,
|
||||
authentication.responseType,
|
||||
authentication.redirectUrl,
|
||||
authentication.scope,
|
||||
authentication.state,
|
||||
authentication.audience,
|
||||
);
|
||||
return _updateOAuth2Token(requestId, results);
|
||||
}
|
||||
|
||||
async function _getOAuth2PasswordHeader(
|
||||
requestId: string,
|
||||
authentication: RequestAuthentication,
|
||||
forceRefresh: boolean,
|
||||
): Promise<OAuth2Token | null> {
|
||||
const oAuth2Token = await _getAccessToken(requestId, authentication, forceRefresh);
|
||||
|
||||
if (oAuth2Token) {
|
||||
return oAuth2Token;
|
||||
}
|
||||
|
||||
const results = await getAccessTokenPassword(
|
||||
requestId,
|
||||
authentication.accessTokenUrl,
|
||||
authentication.credentialsInBody,
|
||||
authentication.clientId,
|
||||
authentication.clientSecret,
|
||||
authentication.username,
|
||||
authentication.password,
|
||||
authentication.scope,
|
||||
authentication.audience,
|
||||
);
|
||||
return _updateOAuth2Token(requestId, results);
|
||||
}
|
||||
|
||||
async function _getAccessToken(
|
||||
requestId: string,
|
||||
authentication: RequestAuthentication,
|
||||
forceRefresh: boolean,
|
||||
): Promise<OAuth2Token | null> {
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// See if we have a token already //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
const token: OAuth2Token | null = await models.oAuth2Token.getByParentId(requestId);
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Check if the token needs refreshing //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Refresh tokens are part of Auth Code, Password
|
||||
const expiresAt = token.expiresAt || Infinity;
|
||||
const isExpired = Date.now() > expiresAt;
|
||||
|
||||
if (!isExpired && !forceRefresh) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// Refresh the token if necessary //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// We've expired, but don't have a refresh token, so tell caller to fetch new
|
||||
// access token
|
||||
if (!token.refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const refreshResults = await refreshAccessToken(
|
||||
requestId,
|
||||
authentication.accessTokenUrl,
|
||||
authentication.credentialsInBody,
|
||||
authentication.clientId,
|
||||
authentication.clientSecret,
|
||||
token.refreshToken,
|
||||
authentication.scope,
|
||||
authentication.origin,
|
||||
);
|
||||
let params = [
|
||||
{ name: 'grant_type', value: 'refresh_token' },
|
||||
{ name: 'refresh_token', value: token.refreshToken },
|
||||
...insertAuthKeyIf('scope', authentication.scope),
|
||||
];
|
||||
const headers = [];
|
||||
if (authentication.credentialsInBody) {
|
||||
params = [
|
||||
...params,
|
||||
...insertAuthKeyIf('client_id', authentication.clientId),
|
||||
...insertAuthKeyIf('client_secret', authentication.clientSecret),
|
||||
];
|
||||
} else {
|
||||
headers.push(getBasicAuthHeader(authentication.clientId, authentication.clientSecret));
|
||||
}
|
||||
const response = await sendAccessTokenRequest(requestId, authentication, params, []);
|
||||
|
||||
// If we didn't receive an access token it means the refresh token didn't succeed,
|
||||
// so we tell caller to fetch brand new access and refresh tokens.
|
||||
if (!refreshResults.access_token) {
|
||||
const statusCode = response.statusCode || 0;
|
||||
const bodyBuffer = models.response.getBodyBuffer(response);
|
||||
|
||||
if (statusCode === 401) {
|
||||
// If the refresh token was rejected due an unauthorized request, we will
|
||||
// return a null access_token to trigger an authentication request to fetch
|
||||
// brand new refresh and access tokens.
|
||||
const old = await models.oAuth2Token.getOrCreateByParentId(requestId);
|
||||
models.oAuth2Token.update(old, transformNewAccessTokenToOauthModel({ access_token: null }));
|
||||
return null;
|
||||
}
|
||||
const isSuccessful = statusCode >= 200 && statusCode < 300;
|
||||
const hasBodyAndIsError = bodyBuffer && statusCode === 400;
|
||||
if (isSuccessful) {
|
||||
if (hasBodyAndIsError) {
|
||||
const body = tryToParse(bodyBuffer.toString());
|
||||
// If the refresh token was rejected due an oauth2 invalid_grant error, we will
|
||||
// return a null access_token to trigger an authentication request to fetch
|
||||
// brand new refresh and access tokens.
|
||||
if (body?.error === 'invalid_grant') {
|
||||
console.log(`[oauth2] Refresh token rejected due to invalid_grant error: ${body.error_description}`);
|
||||
const old = await models.oAuth2Token.getOrCreateByParentId(requestId);
|
||||
models.oAuth2Token.update(old, transformNewAccessTokenToOauthModel({ access_token: null }));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~ //
|
||||
// Update the DB //
|
||||
// ~~~~~~~~~~~~~ //
|
||||
return _updateOAuth2Token(requestId, refreshResults);
|
||||
throw new Error(`[oauth2] Failed to refresh token url=${authentication.accessTokenUrl} status=${statusCode}`);
|
||||
}
|
||||
invariant(bodyBuffer, `[oauth2] No body returned from ${authentication.accessTokenUrl}`);
|
||||
const data = tryToParse(bodyBuffer.toString());
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const old = await models.oAuth2Token.getOrCreateByParentId(requestId);
|
||||
return models.oAuth2Token.update(old, transformNewAccessTokenToOauthModel({
|
||||
...data,
|
||||
refresh_token: data.refresh_token || token.refreshToken,
|
||||
}));
|
||||
}
|
||||
|
||||
async function _updateOAuth2Token(
|
||||
requestId: string,
|
||||
authResults: Record<string, any>,
|
||||
): Promise<OAuth2Token> {
|
||||
const oAuth2Token = await models.oAuth2Token.getOrCreateByParentId(requestId);
|
||||
// Calculate expiry date
|
||||
const expiresIn = authResults[P_EXPIRES_IN];
|
||||
const expiresAt = expiresIn ? Date.now() + expiresIn * 1000 : null;
|
||||
return models.oAuth2Token.update(oAuth2Token, {
|
||||
expiresAt,
|
||||
refreshToken: authResults[P_REFRESH_TOKEN] || null,
|
||||
accessToken: authResults[P_ACCESS_TOKEN] || null,
|
||||
identityToken: authResults[P_ID_TOKEN] || null,
|
||||
error: authResults[P_ERROR] || null,
|
||||
errorDescription: authResults[P_ERROR_DESCRIPTION] || null,
|
||||
errorUri: authResults[P_ERROR_URI] || null,
|
||||
export const oauthResponseToAccessToken = (accessTokenUrl: string, response: Response) => {
|
||||
const bodyBuffer = models.response.getBodyBuffer(response);
|
||||
if (!bodyBuffer) {
|
||||
return {
|
||||
xResponseId: response._id,
|
||||
xError: `No body returned from ${accessTokenUrl}`,
|
||||
};
|
||||
}
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
return {
|
||||
xResponseId: response._id,
|
||||
xError: `Failed to fetch token url=${accessTokenUrl} status=${response.statusCode}`,
|
||||
};
|
||||
}
|
||||
const body = bodyBuffer.toString('utf8');
|
||||
const data = tryToParse(body);
|
||||
return {
|
||||
...data,
|
||||
xResponseId: response._id,
|
||||
};
|
||||
};
|
||||
|
||||
const transformNewAccessTokenToOauthModel = (accessToken: Partial<Record<AuthKeys, string | null>>): Partial<OAuth2Token> => {
|
||||
const expiry = accessToken.expires_in ? +accessToken.expires_in : 0;
|
||||
return {
|
||||
// Calculate expiry date
|
||||
expiresAt: accessToken.expires_in ? Date.now() + expiry * 1000 : null,
|
||||
refreshToken: accessToken.refresh_token || undefined,
|
||||
accessToken: accessToken.access_token || undefined,
|
||||
identityToken: accessToken.id_token || undefined,
|
||||
error: accessToken.error || undefined,
|
||||
errorDescription: accessToken.error_description || undefined,
|
||||
errorUri: accessToken.error_uri || undefined,
|
||||
// Special Cases
|
||||
xResponseId: authResults[X_RESPONSE_ID] || null,
|
||||
xError: authResults[X_ERROR] || null,
|
||||
xResponseId: accessToken.xResponseId || null,
|
||||
xError: accessToken.xError || null,
|
||||
};
|
||||
};
|
||||
|
||||
const sendAccessTokenRequest = async (requestId: string, authentication: AuthTypeOAuth2, params: RequestParameter[], headers: RequestHeader[]) => {
|
||||
invariant(authentication.accessTokenUrl, 'Missing access token URL');
|
||||
const responsePatch = await sendWithSettings(requestId, {
|
||||
headers: [
|
||||
{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' },
|
||||
{ name: 'Accept', value: 'application/x-www-form-urlencoded, application/json' },
|
||||
...headers,
|
||||
],
|
||||
url: setDefaultProtocol(authentication.accessTokenUrl),
|
||||
method: 'POST',
|
||||
body: {
|
||||
mimeType: 'application/x-www-form-urlencoded',
|
||||
params,
|
||||
},
|
||||
});
|
||||
}
|
||||
return await models.response.create(responsePatch);
|
||||
};
|
||||
export const encodePKCE = (buffer: Buffer) => {
|
||||
return buffer.toString('base64')
|
||||
// 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
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
};
|
||||
const tryToParse = (body: string): Record<string, any> | null => {
|
||||
try {
|
||||
return JSON.parse(body);
|
||||
} catch (err) { }
|
||||
|
||||
try {
|
||||
// NOTE: parse does not return a JS Object, so
|
||||
// we cannot use hasOwnProperty on it
|
||||
return querystring.parse(body);
|
||||
} catch (err) { }
|
||||
return null;
|
||||
};
|
||||
|
||||
const insertAuthKeyIf = (name: AuthKeys, value?: string) => value ? [{ name, value }] : [];
|
||||
|
@ -1,297 +0,0 @@
|
||||
import crypto from 'crypto';
|
||||
import { parse as urlParse } from 'url';
|
||||
|
||||
import { escapeRegex } from '../../common/misc';
|
||||
import * as models from '../../models/index';
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../utils/url/querystring';
|
||||
import { getBasicAuthHeader } from '../basic-auth/get-header';
|
||||
import { sendWithSettings } from '../network';
|
||||
import * as c from './constants';
|
||||
import { getOAuthSession, responseToObject } from './misc';
|
||||
export default async function(
|
||||
requestId: string,
|
||||
authorizeUrl: string,
|
||||
accessTokenUrl: string,
|
||||
credentialsInBody: boolean,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
redirectUri = '',
|
||||
scope = '',
|
||||
state = '',
|
||||
audience = '',
|
||||
resource = '',
|
||||
usePkce = false,
|
||||
pkceMethod = c.PKCE_CHALLENGE_S256,
|
||||
origin = '',
|
||||
): Promise<Record<string, any>> {
|
||||
if (!authorizeUrl) {
|
||||
throw new Error('Invalid authorization URL');
|
||||
}
|
||||
|
||||
if (!accessTokenUrl) {
|
||||
throw new Error('Invalid access token URL');
|
||||
}
|
||||
|
||||
let codeVerifier = '';
|
||||
let codeChallenge = '';
|
||||
|
||||
if (usePkce) {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
codeVerifier = _base64UrlEncode(crypto.randomBytes(32));
|
||||
|
||||
if (pkceMethod === c.PKCE_CHALLENGE_S256) {
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
codeChallenge = _base64UrlEncode(crypto.createHash('sha256').update(codeVerifier).digest());
|
||||
} else {
|
||||
codeChallenge = codeVerifier;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const authorizeResults = await _authorize(
|
||||
authorizeUrl,
|
||||
clientId,
|
||||
redirectUri,
|
||||
scope,
|
||||
state,
|
||||
audience,
|
||||
resource,
|
||||
codeChallenge,
|
||||
pkceMethod
|
||||
);
|
||||
|
||||
// Handle the error
|
||||
if (authorizeResults[c.P_ERROR]) {
|
||||
const code = authorizeResults[c.P_ERROR];
|
||||
const msg = authorizeResults[c.P_ERROR_DESCRIPTION];
|
||||
const uri = authorizeResults[c.P_ERROR_URI];
|
||||
throw new Error(`OAuth 2.0 Error ${code}\n\n${msg}\n\n${uri}`);
|
||||
}
|
||||
|
||||
return _getToken(
|
||||
requestId,
|
||||
accessTokenUrl,
|
||||
credentialsInBody,
|
||||
clientId,
|
||||
clientSecret,
|
||||
// @ts-expect-error -- unsound typing
|
||||
authorizeResults[c.P_CODE],
|
||||
redirectUri,
|
||||
state,
|
||||
audience,
|
||||
resource,
|
||||
codeVerifier,
|
||||
origin,
|
||||
);
|
||||
}
|
||||
|
||||
async function _authorize(
|
||||
url: string,
|
||||
clientId: string,
|
||||
redirectUri = '',
|
||||
scope = '',
|
||||
state = '',
|
||||
audience = '',
|
||||
resource = '',
|
||||
codeChallenge = '',
|
||||
pkceMethod = '',
|
||||
) {
|
||||
const params = [
|
||||
{
|
||||
name: c.P_RESPONSE_TYPE,
|
||||
value: c.RESPONSE_TYPE_CODE,
|
||||
},
|
||||
{
|
||||
name: c.P_CLIENT_ID,
|
||||
value: clientId,
|
||||
},
|
||||
];
|
||||
// Add optional params
|
||||
redirectUri &&
|
||||
params.push({
|
||||
name: c.P_REDIRECT_URI,
|
||||
value: redirectUri,
|
||||
});
|
||||
scope &&
|
||||
params.push({
|
||||
name: c.P_SCOPE,
|
||||
value: scope,
|
||||
});
|
||||
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,
|
||||
});
|
||||
|
||||
if (codeChallenge) {
|
||||
params.push({
|
||||
name: c.P_CODE_CHALLENGE,
|
||||
value: codeChallenge,
|
||||
});
|
||||
params.push({
|
||||
name: c.P_CODE_CHALLENGE_METHOD,
|
||||
value: pkceMethod,
|
||||
});
|
||||
}
|
||||
|
||||
// Add query params to URL
|
||||
const qs = buildQueryStringFromParams(params);
|
||||
const finalUrl = joinUrlAndQueryString(url, qs);
|
||||
const urlSuccessRegex = new RegExp(`${escapeRegex(redirectUri)}.*(code=)`, 'i');
|
||||
const urlFailureRegex = new RegExp(`${escapeRegex(redirectUri)}.*(error=)`, 'i');
|
||||
const sessionId = getOAuthSession();
|
||||
|
||||
const redirectedTo = await window.main.authorizeUserInWindow({ url: finalUrl, urlSuccessRegex, urlFailureRegex, sessionId });
|
||||
console.log('[oauth2] Detected redirect ' + redirectedTo);
|
||||
const { query } = urlParse(redirectedTo);
|
||||
return responseToObject(query, [
|
||||
c.P_CODE,
|
||||
c.P_STATE,
|
||||
c.P_ERROR,
|
||||
c.P_ERROR_DESCRIPTION,
|
||||
c.P_ERROR_URI,
|
||||
]);
|
||||
}
|
||||
|
||||
async function _getToken(
|
||||
requestId: string,
|
||||
url: string,
|
||||
credentialsInBody: boolean,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
code: string,
|
||||
redirectUri = '',
|
||||
state = '',
|
||||
audience = '',
|
||||
resource = '',
|
||||
codeVerifier = '',
|
||||
origin = '',
|
||||
): Promise<Record<string, any>> {
|
||||
const params = [
|
||||
{
|
||||
name: c.P_GRANT_TYPE,
|
||||
value: c.GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
},
|
||||
{
|
||||
name: c.P_CODE,
|
||||
value: code,
|
||||
},
|
||||
];
|
||||
// Add optional params
|
||||
redirectUri &&
|
||||
params.push({
|
||||
name: c.P_REDIRECT_URI,
|
||||
value: redirectUri,
|
||||
});
|
||||
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',
|
||||
},
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
},
|
||||
];
|
||||
|
||||
if (credentialsInBody) {
|
||||
params.push({
|
||||
name: c.P_CLIENT_ID,
|
||||
value: clientId,
|
||||
});
|
||||
params.push({
|
||||
name: c.P_CLIENT_SECRET,
|
||||
value: clientSecret,
|
||||
});
|
||||
} else {
|
||||
headers.push(getBasicAuthHeader(clientId, clientSecret));
|
||||
}
|
||||
|
||||
if (origin) {
|
||||
headers.push({ name: 'Origin', value: origin });
|
||||
}
|
||||
|
||||
const responsePatch = await sendWithSettings(requestId, {
|
||||
headers,
|
||||
url,
|
||||
method: 'POST',
|
||||
body: models.request.newBodyFormUrlEncoded(params),
|
||||
});
|
||||
const response = await models.response.create(responsePatch);
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const bodyBuffer = models.response.getBodyBuffer(response);
|
||||
|
||||
if (!bodyBuffer) {
|
||||
return {
|
||||
[c.X_ERROR]: `No body returned from ${url}`,
|
||||
[c.X_RESPONSE_ID]: response._id,
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const statusCode = response.statusCode || 0;
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
return {
|
||||
[c.X_ERROR]: `Failed to fetch token url=${url} status=${statusCode}`,
|
||||
[c.X_RESPONSE_ID]: response._id,
|
||||
};
|
||||
}
|
||||
|
||||
const results = responseToObject(bodyBuffer.toString('utf8'), [
|
||||
c.P_ACCESS_TOKEN,
|
||||
c.P_ID_TOKEN,
|
||||
c.P_REFRESH_TOKEN,
|
||||
c.P_EXPIRES_IN,
|
||||
c.P_TOKEN_TYPE,
|
||||
c.P_SCOPE,
|
||||
c.P_AUDIENCE,
|
||||
c.P_RESOURCE,
|
||||
c.P_ERROR,
|
||||
c.P_ERROR_URI,
|
||||
c.P_ERROR_DESCRIPTION,
|
||||
]);
|
||||
results[c.X_RESPONSE_ID] = response._id;
|
||||
return results;
|
||||
}
|
||||
|
||||
function _base64UrlEncode(str: string) {
|
||||
// @ts-expect-error -- TSCONVERSION appears to be genuine
|
||||
return str.toString('base64')
|
||||
// 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
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=/g, '');
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
import * as models from '../../models/index';
|
||||
import { setDefaultProtocol } from '../../utils/url/protocol';
|
||||
import { getBasicAuthHeader } from '../basic-auth/get-header';
|
||||
import { sendWithSettings } from '../network';
|
||||
import * as c from './constants';
|
||||
import { responseToObject } from './misc';
|
||||
|
||||
export default async function(
|
||||
requestId: string,
|
||||
accessTokenUrl: string,
|
||||
credentialsInBody: boolean,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
scope = '',
|
||||
audience = '',
|
||||
resource = '',
|
||||
): Promise<Record<string, any>> {
|
||||
const params = [
|
||||
{
|
||||
name: c.P_GRANT_TYPE,
|
||||
value: c.GRANT_TYPE_CLIENT_CREDENTIALS,
|
||||
},
|
||||
];
|
||||
// Add optional params
|
||||
scope &&
|
||||
params.push({
|
||||
name: c.P_SCOPE,
|
||||
value: scope,
|
||||
});
|
||||
audience &&
|
||||
params.push({
|
||||
name: c.P_AUDIENCE,
|
||||
value: audience,
|
||||
});
|
||||
resource &&
|
||||
params.push({
|
||||
name: c.P_RESOURCE,
|
||||
value: resource,
|
||||
});
|
||||
const headers = [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
},
|
||||
];
|
||||
|
||||
if (credentialsInBody) {
|
||||
params.push({
|
||||
name: c.P_CLIENT_ID,
|
||||
value: clientId,
|
||||
});
|
||||
params.push({
|
||||
name: c.P_CLIENT_SECRET,
|
||||
value: clientSecret,
|
||||
});
|
||||
} else {
|
||||
headers.push(getBasicAuthHeader(clientId, clientSecret));
|
||||
}
|
||||
|
||||
const url = setDefaultProtocol(accessTokenUrl);
|
||||
const responsePatch = await sendWithSettings(requestId, {
|
||||
headers,
|
||||
url,
|
||||
method: 'POST',
|
||||
body: models.request.newBodyFormUrlEncoded(params),
|
||||
});
|
||||
const response = await models.response.create(responsePatch);
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const bodyBuffer = models.response.getBodyBuffer(response);
|
||||
|
||||
if (!bodyBuffer) {
|
||||
return {
|
||||
[c.X_ERROR]: `No body returned from ${url}`,
|
||||
[c.X_RESPONSE_ID]: response._id,
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const statusCode = response.statusCode || 0;
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
return {
|
||||
[c.X_ERROR]: `Failed to fetch token url=${url} status=${statusCode}`,
|
||||
[c.X_RESPONSE_ID]: response._id,
|
||||
};
|
||||
}
|
||||
|
||||
const results = responseToObject(bodyBuffer.toString('utf8'), [
|
||||
c.P_ACCESS_TOKEN,
|
||||
c.P_ID_TOKEN,
|
||||
c.P_REFRESH_TOKEN,
|
||||
c.P_TOKEN_TYPE,
|
||||
c.P_EXPIRES_IN,
|
||||
c.P_SCOPE,
|
||||
c.P_AUDIENCE,
|
||||
c.P_RESOURCE,
|
||||
c.P_ERROR,
|
||||
c.P_ERROR_URI,
|
||||
c.P_ERROR_DESCRIPTION,
|
||||
]);
|
||||
results[c.X_RESPONSE_ID] = response._id;
|
||||
return results;
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
import { buildQueryStringFromParams, joinUrlAndQueryString } from '../../utils/url/querystring';
|
||||
import * as c from './constants';
|
||||
import { getOAuthSession, responseToObject } from './misc';
|
||||
|
||||
export default async function(
|
||||
_requestId: string,
|
||||
authorizationUrl: string,
|
||||
clientId: string,
|
||||
responseType: string = c.RESPONSE_TYPE_TOKEN,
|
||||
redirectUri = '',
|
||||
scope = '',
|
||||
state = '',
|
||||
audience = '',
|
||||
): Promise<Record<string, any>> {
|
||||
const params = [
|
||||
{
|
||||
name: c.P_RESPONSE_TYPE,
|
||||
value: responseType,
|
||||
},
|
||||
{
|
||||
name: c.P_CLIENT_ID,
|
||||
value: clientId,
|
||||
},
|
||||
];
|
||||
|
||||
// Add optional params
|
||||
if (
|
||||
responseType === c.RESPONSE_TYPE_ID_TOKEN_TOKEN ||
|
||||
responseType === c.RESPONSE_TYPE_ID_TOKEN
|
||||
) {
|
||||
const nonce = Math.floor(Math.random() * 9999999999999) + 1;
|
||||
params.push({
|
||||
name: c.P_NONCE,
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
value: nonce,
|
||||
});
|
||||
}
|
||||
|
||||
redirectUri &&
|
||||
params.push({
|
||||
name: c.P_REDIRECT_URI,
|
||||
value: redirectUri,
|
||||
});
|
||||
scope &&
|
||||
params.push({
|
||||
name: c.P_SCOPE,
|
||||
value: scope,
|
||||
});
|
||||
state &&
|
||||
params.push({
|
||||
name: c.P_STATE,
|
||||
value: state,
|
||||
});
|
||||
audience &&
|
||||
params.push({
|
||||
name: c.P_AUDIENCE,
|
||||
value: audience,
|
||||
});
|
||||
// Add query params to URL
|
||||
const qs = buildQueryStringFromParams(params);
|
||||
const finalUrl = joinUrlAndQueryString(authorizationUrl, qs);
|
||||
const urlSuccessRegex = /(access_token=|id_token=)/;
|
||||
const urlFailureRegex = /(error=)/;
|
||||
const sessionId = getOAuthSession();
|
||||
const redirectedTo = await window.main.authorizeUserInWindow({ url: finalUrl, urlSuccessRegex, urlFailureRegex, sessionId });
|
||||
|
||||
const fragment = redirectedTo.split('#')[1];
|
||||
|
||||
if (fragment) {
|
||||
const results = responseToObject(fragment, [
|
||||
c.P_ACCESS_TOKEN,
|
||||
c.P_ID_TOKEN,
|
||||
c.P_TOKEN_TYPE,
|
||||
c.P_EXPIRES_IN,
|
||||
c.P_SCOPE,
|
||||
c.P_STATE,
|
||||
c.P_ERROR,
|
||||
c.P_ERROR_DESCRIPTION,
|
||||
c.P_ERROR_URI,
|
||||
]);
|
||||
results[c.P_ACCESS_TOKEN] = results[c.P_ACCESS_TOKEN] || results[c.P_ID_TOKEN];
|
||||
return results;
|
||||
} else {
|
||||
// Bad redirect
|
||||
return {};
|
||||
}
|
||||
}
|
@ -1,110 +0,0 @@
|
||||
import * as models from '../../models/index';
|
||||
import { setDefaultProtocol } from '../../utils/url/protocol';
|
||||
import { getBasicAuthHeader } from '../basic-auth/get-header';
|
||||
import * as network from '../network';
|
||||
import * as c from './constants';
|
||||
import { responseToObject } from './misc';
|
||||
|
||||
export default async function(
|
||||
requestId: string,
|
||||
accessTokenUrl: string,
|
||||
credentialsInBody: boolean,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
username: string,
|
||||
password: string,
|
||||
scope = '',
|
||||
audience = '',
|
||||
): Promise<Record<string, any>> {
|
||||
const params = [
|
||||
{
|
||||
name: c.P_GRANT_TYPE,
|
||||
value: c.GRANT_TYPE_PASSWORD,
|
||||
},
|
||||
{
|
||||
name: c.P_USERNAME,
|
||||
value: username,
|
||||
},
|
||||
{
|
||||
name: c.P_PASSWORD,
|
||||
value: password,
|
||||
},
|
||||
];
|
||||
// Add optional params
|
||||
scope &&
|
||||
params.push({
|
||||
name: c.P_SCOPE,
|
||||
value: scope,
|
||||
});
|
||||
audience &&
|
||||
params.push({
|
||||
name: c.P_AUDIENCE,
|
||||
value: audience,
|
||||
});
|
||||
const headers = [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
},
|
||||
];
|
||||
|
||||
if (credentialsInBody) {
|
||||
params.push({
|
||||
name: c.P_CLIENT_ID,
|
||||
value: clientId,
|
||||
});
|
||||
params.push({
|
||||
name: c.P_CLIENT_SECRET,
|
||||
value: clientSecret,
|
||||
});
|
||||
} else {
|
||||
headers.push(getBasicAuthHeader(clientId, clientSecret));
|
||||
}
|
||||
|
||||
const url = setDefaultProtocol(accessTokenUrl);
|
||||
const responsePatch = await network.sendWithSettings(requestId, {
|
||||
url,
|
||||
headers,
|
||||
method: 'POST',
|
||||
body: models.request.newBodyFormUrlEncoded(params),
|
||||
});
|
||||
const response = await models.response.create(responsePatch);
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const bodyBuffer = models.response.getBodyBuffer(response);
|
||||
|
||||
if (!bodyBuffer) {
|
||||
return {
|
||||
[c.X_ERROR]: `No body returned from ${url}`,
|
||||
[c.X_RESPONSE_ID]: response._id,
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-expect-error -- TSCONVERSION
|
||||
const statusCode = response.statusCode || 0;
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
return {
|
||||
[c.X_ERROR]: `Failed to fetch token url=${url} status=${statusCode}`,
|
||||
[c.X_RESPONSE_ID]: response._id,
|
||||
};
|
||||
}
|
||||
|
||||
const results = responseToObject(bodyBuffer.toString(), [
|
||||
c.P_ACCESS_TOKEN,
|
||||
c.P_ID_TOKEN,
|
||||
c.P_TOKEN_TYPE,
|
||||
c.P_EXPIRES_IN,
|
||||
c.P_REFRESH_TOKEN,
|
||||
c.P_SCOPE,
|
||||
c.P_AUDIENCE,
|
||||
c.P_ERROR,
|
||||
c.P_ERROR_URI,
|
||||
c.P_ERROR_DESCRIPTION,
|
||||
]);
|
||||
results[c.X_RESPONSE_ID] = response._id;
|
||||
return results;
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import electron from 'electron';
|
||||
import querystring from 'querystring';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import * as models from '../../models/index';
|
||||
@ -22,45 +21,6 @@ export function initNewOAuthSession() {
|
||||
return authWindowSessionId;
|
||||
}
|
||||
|
||||
export function responseToObject(body: string | null, keys: string[], defaults: Record<string, string | string[]> = {}) {
|
||||
let data: querystring.ParsedUrlQuery | null = null;
|
||||
|
||||
try {
|
||||
// TODO: remove non-null assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
data = JSON.parse(body!);
|
||||
} catch (err) {}
|
||||
|
||||
if (!data) {
|
||||
try {
|
||||
// NOTE: parse does not return a JS Object, so
|
||||
// we cannot use hasOwnProperty on it
|
||||
// TODO: remove non-null assertion
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
data = querystring.parse(body!);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
// Shouldn't happen but we'll check anyway
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
|
||||
const results: Record<string, string | string[] | null | undefined> = {};
|
||||
|
||||
for (const key of keys) {
|
||||
if (data[key] !== undefined) {
|
||||
results[key] = data[key];
|
||||
} else if (defaults?.hasOwnProperty(key)) {
|
||||
results[key] = defaults[key];
|
||||
} else {
|
||||
results[key] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function authorizeUserInWindow({
|
||||
url,
|
||||
urlSuccessRegex = /(code=).*/,
|
||||
|
@ -1,114 +0,0 @@
|
||||
import * as models from '../../models/index';
|
||||
import { setDefaultProtocol } from '../../utils/url/protocol';
|
||||
import { getBasicAuthHeader } from '../basic-auth/get-header';
|
||||
import { sendWithSettings } from '../network';
|
||||
import * as c from './constants';
|
||||
import { responseToObject } from './misc';
|
||||
|
||||
export default async function(
|
||||
requestId: string,
|
||||
accessTokenUrl: string,
|
||||
credentialsInBody: boolean,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
refreshToken: string,
|
||||
scope: string,
|
||||
origin: string,
|
||||
): Promise<Record<string, any>> {
|
||||
const params = [
|
||||
{
|
||||
name: c.P_GRANT_TYPE,
|
||||
value: c.GRANT_TYPE_REFRESH,
|
||||
},
|
||||
{
|
||||
name: c.P_REFRESH_TOKEN,
|
||||
value: refreshToken,
|
||||
},
|
||||
];
|
||||
// Add optional params
|
||||
scope &&
|
||||
params.push({
|
||||
name: c.P_SCOPE,
|
||||
value: scope,
|
||||
});
|
||||
const headers = [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
},
|
||||
{
|
||||
name: 'Accept',
|
||||
value: 'application/x-www-form-urlencoded, application/json',
|
||||
},
|
||||
];
|
||||
|
||||
if (credentialsInBody) {
|
||||
params.push({
|
||||
name: c.P_CLIENT_ID,
|
||||
value: clientId,
|
||||
});
|
||||
params.push({
|
||||
name: c.P_CLIENT_SECRET,
|
||||
value: clientSecret,
|
||||
});
|
||||
} else {
|
||||
headers.push(getBasicAuthHeader(clientId, clientSecret));
|
||||
}
|
||||
|
||||
if (origin) {
|
||||
headers.push({ name: 'Origin', value: origin });
|
||||
}
|
||||
|
||||
const url = setDefaultProtocol(accessTokenUrl);
|
||||
const response = await sendWithSettings(requestId, {
|
||||
headers,
|
||||
url,
|
||||
method: 'POST',
|
||||
body: models.request.newBodyFormUrlEncoded(params),
|
||||
});
|
||||
const statusCode = response.statusCode || 0;
|
||||
const bodyBuffer = models.response.getBodyBuffer(response);
|
||||
|
||||
if (statusCode === 401) {
|
||||
// If the refresh token was rejected due an unauthorized request, we will
|
||||
// return a null access_token to trigger an authentication request to fetch
|
||||
// brand new refresh and access tokens.
|
||||
return responseToObject(null, [c.P_ACCESS_TOKEN]);
|
||||
} else if (statusCode < 200 || statusCode >= 300) {
|
||||
if (bodyBuffer && statusCode === 400) {
|
||||
const response = responseToObject(bodyBuffer.toString(), [c.P_ERROR, c.P_ERROR_DESCRIPTION]);
|
||||
|
||||
// If the refresh token was rejected due an oauth2 invalid_grant error, we will
|
||||
// return a null access_token to trigger an authentication request to fetch
|
||||
// brand new refresh and access tokens.
|
||||
if (response[c.P_ERROR] === 'invalid_grant') {
|
||||
return responseToObject(null, [c.P_ACCESS_TOKEN]);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`[oauth2] Failed to refresh token url=${url} status=${statusCode}`);
|
||||
}
|
||||
|
||||
if (!bodyBuffer) {
|
||||
throw new Error(`[oauth2] No body returned from ${url}`);
|
||||
}
|
||||
|
||||
return responseToObject(
|
||||
bodyBuffer.toString(),
|
||||
[
|
||||
c.P_ACCESS_TOKEN,
|
||||
c.P_ID_TOKEN,
|
||||
c.P_REFRESH_TOKEN,
|
||||
c.P_EXPIRES_IN,
|
||||
c.P_TOKEN_TYPE,
|
||||
c.P_SCOPE,
|
||||
c.P_ERROR,
|
||||
c.P_ERROR_URI,
|
||||
c.P_ERROR_DESCRIPTION,
|
||||
],
|
||||
{
|
||||
// Refresh token is optional, so we'll default it to the existing value
|
||||
[c.P_REFRESH_TOKEN]: refreshToken,
|
||||
},
|
||||
);
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
import React, { ChangeEvent, FC, ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React, { ChangeEvent, FC, ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { convertEpochToMilliseconds, toKebabCase } from '../../../../common/misc';
|
||||
import accessTokenUrls from '../../../../datasets/access-token-urls';
|
||||
import authorizationUrls from '../../../../datasets/authorization-urls';
|
||||
import * as models from '../../../../models';
|
||||
import type { OAuth2Token } from '../../../../models/o-auth-2-token';
|
||||
import type { Request } from '../../../../models/request';
|
||||
import type { AuthTypeOAuth2, Request } from '../../../../models/request';
|
||||
import {
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
GRANT_TYPE_CLIENT_CREDENTIALS,
|
||||
@ -18,11 +17,10 @@ import {
|
||||
RESPONSE_TYPE_ID_TOKEN_TOKEN,
|
||||
RESPONSE_TYPE_TOKEN,
|
||||
} from '../../../../network/o-auth-2/constants';
|
||||
import getAccessToken from '../../../../network/o-auth-2/get-token';
|
||||
import { getOAuth2Token } from '../../../../network/o-auth-2/get-token';
|
||||
import { initNewOAuthSession } from '../../../../network/o-auth-2/misc';
|
||||
import { useNunjucks } from '../../../context/nunjucks/use-nunjucks';
|
||||
import { useActiveRequest } from '../../../hooks/use-active-request';
|
||||
import { selectActiveOAuth2Token } from '../../../redux/selectors';
|
||||
import { Link } from '../../base/link';
|
||||
import { showModal } from '../../modals';
|
||||
import { ResponseDebugModal } from '../../modals/response-debug-modal';
|
||||
@ -105,12 +103,12 @@ const getFields = (authentication: Request['authentication']) => {
|
||||
options={pkceMethodOptions}
|
||||
/>;
|
||||
const authorizationUrl = <AuthInputRow label='Authorization URL' property='authorizationUrl' key='authorizationUrl' getAutocompleteConstants={getAuthorizationUrls} />;
|
||||
const accessTokenUrl = <AuthInputRow label='Access Token URL' property='accessTokenUrl' key='accessTokenUrl' getAutocompleteConstants={getAccessTokenUrls} />;
|
||||
const accessTokenUrl = <AuthInputRow label='Access Token URL' property='accessTokenUrl' key='accessTokenUrl' getAutocompleteConstants={getAccessTokenUrls} />;
|
||||
const redirectUri = <AuthInputRow label='Redirect URL' property='redirectUrl' key='redirectUrl' help='This can be whatever you want or need it to be. Insomnia will automatically detect a redirect in the client browser window and extract the code from the redirected URL.' />;
|
||||
const state = <AuthInputRow label='State' property='state' key='state' />;
|
||||
const scope = <AuthInputRow label='Scope' property='scope' key='scope' />;
|
||||
const username = <AuthInputRow label='Username' property='username' key='username' />;
|
||||
const password = <AuthInputRow label='Password' property='password' key='password' mask/>;
|
||||
const password = <AuthInputRow label='Password' property='password' key='password' mask />;
|
||||
const tokenPrefix = <AuthInputRow label='Header Prefix' property='tokenPrefix' key='tokenPrefix' help='Change Authorization header prefix from "Bearer" to something else. Use "NO_PREFIX" to send raw token without prefix.' />;
|
||||
const responseType = <AuthSelectRow
|
||||
label='Response Type'
|
||||
@ -266,7 +264,7 @@ export const OAuth2Auth: FC = () => {
|
||||
{advanced}
|
||||
{
|
||||
<tr>
|
||||
<td/>
|
||||
<td />
|
||||
<td className="wide">
|
||||
<div className="pad-top text-right">
|
||||
<button className="btn btn--clicky" onClick={initNewOAuthSession}>
|
||||
@ -334,9 +332,8 @@ const renderAccessTokenExpiry = (token?: Pick<OAuth2Token, 'accessToken' | 'expi
|
||||
);
|
||||
};
|
||||
|
||||
const OAuth2TokenInput: FC<{label: string; property: keyof Pick<OAuth2Token, 'accessToken' | 'refreshToken' | 'identityToken'>}> = ({ label, property }) => {
|
||||
const OAuth2TokenInput: FC<{ token: OAuth2Token | null; label: string; property: keyof Pick<OAuth2Token, 'accessToken' | 'refreshToken' | 'identityToken'> }> = ({ token, label, property }) => {
|
||||
const { activeRequest } = useActiveRequest();
|
||||
const token = useSelector(selectActiveOAuth2Token);
|
||||
|
||||
const onChange = async ({ currentTarget: { value } }: ChangeEvent<HTMLInputElement>) => {
|
||||
if (token) {
|
||||
@ -348,9 +345,9 @@ const OAuth2TokenInput: FC<{label: string; property: keyof Pick<OAuth2Token, 'ac
|
||||
|
||||
const expiryLabel = useMemo(() => {
|
||||
if (property === 'identityToken') {
|
||||
return renderIdentityTokenExpiry(token);
|
||||
return token && renderIdentityTokenExpiry(token);
|
||||
} else if (property === 'accessToken') {
|
||||
return renderAccessTokenExpiry(token);
|
||||
return token && renderAccessTokenExpiry(token);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@ -372,9 +369,7 @@ const OAuth2TokenInput: FC<{label: string; property: keyof Pick<OAuth2Token, 'ac
|
||||
);
|
||||
};
|
||||
|
||||
const OAuth2Error: FC = () => {
|
||||
const token = useSelector(selectActiveOAuth2Token);
|
||||
|
||||
const OAuth2Error: FC<{ token: OAuth2Token | null }> = ({ token }) => {
|
||||
const debug = () => {
|
||||
if (!token || !token.xResponseId) {
|
||||
return;
|
||||
@ -420,42 +415,20 @@ const OAuth2Error: FC = () => {
|
||||
return debugButton;
|
||||
};
|
||||
|
||||
const useActiveOAuth2Token = () => {
|
||||
const token = useSelector(selectActiveOAuth2Token);
|
||||
const OAuth2Tokens: FC = () => {
|
||||
const { activeRequest: { authentication, _id: requestId } } = useActiveRequest();
|
||||
const [token, setToken] = useState<OAuth2Token | null>(null);
|
||||
useEffect(() => {
|
||||
const fn = async () => {
|
||||
const token = await models.oAuth2Token.getByParentId(requestId);
|
||||
setToken(token);
|
||||
};
|
||||
fn();
|
||||
}, [requestId]);
|
||||
const { handleRender } = useNunjucks();
|
||||
|
||||
const clearTokens = useCallback(async () => {
|
||||
if (token) {
|
||||
await models.oAuth2Token.remove(token);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const refreshToken = useCallback(async () => {
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const renderedAuthentication = await handleRender(authentication);
|
||||
await getAccessToken(requestId, renderedAuthentication, true);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
// Clear existing tokens if there's an error
|
||||
await clearTokens();
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [authentication, clearTokens, handleRender, requestId]);
|
||||
|
||||
return { error, loading, token, clearTokens, refreshToken };
|
||||
};
|
||||
|
||||
const OAuth2Tokens: FC = () => {
|
||||
const { token, clearTokens, refreshToken, loading, error } = useActiveOAuth2Token();
|
||||
|
||||
return (
|
||||
<div className='notice subtle text-left'>
|
||||
{error && (
|
||||
@ -463,20 +436,47 @@ const OAuth2Tokens: FC = () => {
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<OAuth2Error />
|
||||
<OAuth2TokenInput label='Refresh Token' property='refreshToken' />
|
||||
<OAuth2TokenInput label='Identity Token' property='identityToken' />
|
||||
<OAuth2TokenInput label='Access Token' property='accessToken' />
|
||||
<OAuth2Error token={token} />
|
||||
<OAuth2TokenInput token={token} label='Refresh Token' property='refreshToken' />
|
||||
<OAuth2TokenInput token={token} label='Identity Token' property='identityToken' />
|
||||
<OAuth2TokenInput token={token} label='Access Token' property='accessToken' />
|
||||
<div className='pad-top text-right'>
|
||||
{token ? (
|
||||
<button className="btn btn--clicky" onClick={clearTokens}>
|
||||
<button
|
||||
className="btn btn--clicky"
|
||||
disabled={!token}
|
||||
onClick={() => {
|
||||
if (token) {
|
||||
setToken(null);
|
||||
models.oAuth2Token.remove(token);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
className="btn btn--clicky"
|
||||
onClick={refreshToken}
|
||||
onClick={async () => {
|
||||
setError('');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const renderedAuthentication = await handleRender(authentication) as AuthTypeOAuth2;
|
||||
const t = await getOAuth2Token(requestId, renderedAuthentication, true);
|
||||
setToken(t);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
// Clear existing tokens if there's an error
|
||||
if (token) {
|
||||
setToken(null);
|
||||
models.oAuth2Token.remove(token);
|
||||
}
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading
|
||||
|
@ -16,12 +16,6 @@ import type {
|
||||
Request,
|
||||
RequestBodyParameter,
|
||||
} from '../../../../models/request';
|
||||
import {
|
||||
newBodyFile,
|
||||
newBodyForm,
|
||||
newBodyFormUrlEncoded,
|
||||
newBodyRaw,
|
||||
} from '../../../../models/request';
|
||||
import type { Workspace } from '../../../../models/workspace';
|
||||
import { NunjucksEnabledProvider } from '../../../context/nunjucks/nunjucks-enabled-context';
|
||||
import { AskModal } from '../../modals/ask-modal';
|
||||
@ -46,24 +40,51 @@ export const BodyEditor: FC<Props> = ({
|
||||
environmentId,
|
||||
}) => {
|
||||
const handleRawChange = useCallback((rawValue: string) => {
|
||||
models.request.update(request, { body: newBodyRaw(rawValue, request.body.mimeType || '') });
|
||||
models.request.update(request, {
|
||||
body: typeof request.body.mimeType !== 'string' ? {
|
||||
text: rawValue,
|
||||
} : {
|
||||
mimeType: request.body.mimeType.split(';')[0],
|
||||
text: rawValue,
|
||||
},
|
||||
});
|
||||
}, [request]);
|
||||
|
||||
const handleGraphQLChange = useCallback((content: string) => {
|
||||
models.request.update(request, { body: newBodyRaw(content, CONTENT_TYPE_GRAPHQL) });
|
||||
models.request.update(request, {
|
||||
body: typeof CONTENT_TYPE_GRAPHQL !== 'string' ? {
|
||||
text: content,
|
||||
} : {
|
||||
mimeType: CONTENT_TYPE_GRAPHQL.split(';')[0],
|
||||
text: content,
|
||||
},
|
||||
});
|
||||
}, [request]);
|
||||
|
||||
const handleFormUrlEncodedChange = useCallback((parameters: RequestBodyParameter[]) => {
|
||||
models.request.update(request, { body: newBodyFormUrlEncoded(parameters) });
|
||||
const handleFormUrlEncodedChange = useCallback((params: RequestBodyParameter[]) => {
|
||||
models.request.update(request, {
|
||||
body: {
|
||||
mimeType: CONTENT_TYPE_FORM_URLENCODED,
|
||||
params,
|
||||
},
|
||||
});
|
||||
}, [request]);
|
||||
|
||||
const handleFormChange = useCallback((parameters: RequestBodyParameter[]) => {
|
||||
models.request.update(request, { body: newBodyForm(parameters) });
|
||||
models.request.update(request, {
|
||||
body: {
|
||||
mimeType: CONTENT_TYPE_FORM_DATA,
|
||||
params: parameters || [],
|
||||
},
|
||||
});
|
||||
}, [request]);
|
||||
|
||||
const handleFileChange = async (path: string) => {
|
||||
const headers = clone(request.headers);
|
||||
const body = newBodyFile(path);
|
||||
const body = {
|
||||
mimeType: CONTENT_TYPE_FILE,
|
||||
fileName: path,
|
||||
};
|
||||
const newRequest = await models.request.update(request, { body });
|
||||
let contentTypeHeader = getContentTypeHeader(headers);
|
||||
|
||||
|
@ -21,6 +21,7 @@ import type { ResponsePatch } from '../../../../main/network/libcurl-promise';
|
||||
import * as models from '../../../../models';
|
||||
import type { Request } from '../../../../models/request';
|
||||
import * as network from '../../../../network/network';
|
||||
import { invariant } from '../../../../utils/invariant';
|
||||
import { jsonPrettify } from '../../../../utils/prettify/json';
|
||||
import { selectSettings } from '../../../redux/selectors';
|
||||
import { Dropdown } from '../../base/dropdown/dropdown';
|
||||
@ -34,11 +35,6 @@ import { HelpTooltip } from '../../help-tooltip';
|
||||
import { Toolbar } from '../../key-value-editor/key-value-editor';
|
||||
import { useDocBodyKeyboardShortcuts } from '../../keydown-binder';
|
||||
import { TimeFromNow } from '../../time-from-now';
|
||||
const explorerContainer = document.querySelector('#graphql-explorer-container');
|
||||
|
||||
if (!explorerContainer) {
|
||||
throw new Error('Failed to find #graphql-explorer-container');
|
||||
}
|
||||
|
||||
const isOperationDefinition = (def: DefinitionNode): def is OperationDefinitionNode => def.kind === Kind.OPERATION_DEFINITION;
|
||||
|
||||
@ -361,6 +357,8 @@ export const GraphQLEditor: FC<Props> = ({
|
||||
|
||||
// Create portal for GraphQL Explorer
|
||||
let graphQLExplorerPortal: React.ReactPortal | null = null;
|
||||
const explorerContainer = document.querySelector('#graphql-explorer-container');
|
||||
invariant(explorerContainer, 'Failed to find #graphql-explorer-container');
|
||||
if (explorerContainer) {
|
||||
graphQLExplorerPortal = ReactDOM.createPortal(
|
||||
<GraphQLExplorer
|
||||
|
@ -2,14 +2,15 @@ import React, { FC, useCallback, useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getContentTypeFromHeaders } from '../../../common/constants';
|
||||
import { CONTENT_TYPE_FILE, CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, CONTENT_TYPE_GRAPHQL, CONTENT_TYPE_JSON, CONTENT_TYPE_OTHER, getContentTypeFromHeaders, METHOD_POST } from '../../../common/constants';
|
||||
import { database } from '../../../common/database';
|
||||
import { getContentTypeHeader } from '../../../common/misc';
|
||||
import * as models from '../../../models';
|
||||
import { queryAllWorkspaceUrls } from '../../../models/helpers/query-all-workspace-urls';
|
||||
import { update } from '../../../models/helpers/request-operations';
|
||||
import type { Request } from '../../../models/request';
|
||||
import { Request, RequestBody } from '../../../models/request';
|
||||
import type { Settings } from '../../../models/settings';
|
||||
import type { Workspace } from '../../../models/workspace';
|
||||
import { create, Workspace } from '../../../models/workspace';
|
||||
import { deconstructQueryStringToParams, extractQueryStringFromUrl } from '../../../utils/url/querystring';
|
||||
import { useActiveRequestSyncVCSVersion, useGitVCSVersion } from '../../hooks/use-vcs-version';
|
||||
import { selectActiveEnvironment, selectActiveRequestMeta } from '../../redux/selectors';
|
||||
@ -64,7 +65,139 @@ interface Props {
|
||||
workspace: Workspace;
|
||||
setLoading: (l: boolean) => void;
|
||||
}
|
||||
export function newBodyGraphQL(rawBody: string): RequestBody {
|
||||
try {
|
||||
// Only strip the newlines if rawBody is a parsable JSON
|
||||
JSON.parse(rawBody);
|
||||
return {
|
||||
mimeType: CONTENT_TYPE_GRAPHQL,
|
||||
text: rawBody.replace(/\\\\n/g, ''),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
return {
|
||||
mimeType: CONTENT_TYPE_GRAPHQL,
|
||||
text: rawBody,
|
||||
};
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
export function updateMimeType(
|
||||
request: Request,
|
||||
mimeType: string,
|
||||
doCreate = false,
|
||||
savedBody: RequestBody = {},
|
||||
) {
|
||||
let headers = request.headers ? [...request.headers] : [];
|
||||
const contentTypeHeader = getContentTypeHeader(headers);
|
||||
// GraphQL uses JSON content-type
|
||||
const contentTypeHeaderValue = mimeType === CONTENT_TYPE_GRAPHQL ? CONTENT_TYPE_JSON : mimeType;
|
||||
|
||||
// GraphQL must be POST
|
||||
if (mimeType === CONTENT_TYPE_GRAPHQL) {
|
||||
request.method = METHOD_POST;
|
||||
}
|
||||
|
||||
// Check if we are converting to/from variants of XML or JSON
|
||||
let leaveContentTypeAlone = false;
|
||||
|
||||
if (contentTypeHeader && mimeType) {
|
||||
const current = contentTypeHeader.value;
|
||||
|
||||
if (current.includes('xml') && mimeType.includes('xml')) {
|
||||
leaveContentTypeAlone = true;
|
||||
} else if (current.includes('json') && mimeType.includes('json')) {
|
||||
leaveContentTypeAlone = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// 1. Update Content-Type header //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
const hasBody = typeof mimeType === 'string';
|
||||
|
||||
if (!hasBody) {
|
||||
headers = headers.filter(h => h !== contentTypeHeader);
|
||||
} else if (mimeType === CONTENT_TYPE_OTHER) {
|
||||
// Leave headers alone
|
||||
} else if (mimeType && contentTypeHeader && !leaveContentTypeAlone) {
|
||||
contentTypeHeader.value = contentTypeHeaderValue;
|
||||
} else if (mimeType && !contentTypeHeader) {
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: contentTypeHeaderValue,
|
||||
});
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// 2. Make a new request body //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
let body;
|
||||
const oldBody = Object.keys(savedBody).length === 0 ? request.body : savedBody;
|
||||
|
||||
if (mimeType === CONTENT_TYPE_FORM_URLENCODED) {
|
||||
// Urlencoded
|
||||
body = oldBody.params
|
||||
? {
|
||||
mimeType: CONTENT_TYPE_FORM_URLENCODED,
|
||||
params: oldBody.params,
|
||||
} : {
|
||||
mimeType: CONTENT_TYPE_FORM_URLENCODED,
|
||||
params: oldBody.text ? deconstructQueryStringToParams(oldBody.text) : [],
|
||||
};
|
||||
} else if (mimeType === CONTENT_TYPE_FORM_DATA) {
|
||||
// Form Data
|
||||
body = oldBody.params
|
||||
? {
|
||||
mimeType: CONTENT_TYPE_FORM_DATA,
|
||||
params: oldBody.params || [],
|
||||
} : {
|
||||
mimeType: CONTENT_TYPE_FORM_DATA,
|
||||
params: oldBody.text ? deconstructQueryStringToParams(oldBody.text) : [],
|
||||
};
|
||||
} else if (mimeType === CONTENT_TYPE_FILE) {
|
||||
// File
|
||||
body = {
|
||||
mimeType: CONTENT_TYPE_FILE,
|
||||
fileName: '',
|
||||
};
|
||||
} else if (mimeType === CONTENT_TYPE_GRAPHQL) {
|
||||
if (contentTypeHeader) {
|
||||
contentTypeHeader.value = CONTENT_TYPE_JSON;
|
||||
}
|
||||
|
||||
body = newBodyGraphQL(oldBody.text || '');
|
||||
} else if (typeof mimeType !== 'string') {
|
||||
// No body
|
||||
body = {};
|
||||
} else {
|
||||
// Raw Content-Type (ex: application/json)
|
||||
body = typeof mimeType !== 'string' ? {
|
||||
text: oldBody.text || '',
|
||||
} : {
|
||||
mimeType: mimeType.split(';')[0],
|
||||
text: oldBody.text || '',
|
||||
};
|
||||
}
|
||||
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
// 2. create/update request //
|
||||
// ~~~~~~~~~~~~~~~~~~~~~~~~ //
|
||||
if (doCreate) {
|
||||
const newRequest: Request = Object.assign({}, request, {
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
return create(newRequest);
|
||||
} else {
|
||||
return update(request, {
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
||||
export const RequestPane: FC<Props> = ({
|
||||
environmentId,
|
||||
request,
|
||||
@ -89,11 +222,11 @@ export const RequestPane: FC<Props> = ({
|
||||
}, [handleEditDescription]);
|
||||
|
||||
const handleUpdateSettingsUseBulkHeaderEditor = useCallback(() => {
|
||||
models.settings.update(settings, { useBulkHeaderEditor:!settings.useBulkHeaderEditor });
|
||||
models.settings.update(settings, { useBulkHeaderEditor: !settings.useBulkHeaderEditor });
|
||||
}, [settings]);
|
||||
|
||||
const handleUpdateSettingsUseBulkParametersEditor = useCallback(() => {
|
||||
models.settings.update(settings, { useBulkParametersEditor:!settings.useBulkParametersEditor });
|
||||
models.settings.update(settings, { useBulkParametersEditor: !settings.useBulkParametersEditor });
|
||||
}, [settings]);
|
||||
|
||||
const handleImportQueryFromUrl = useCallback(() => {
|
||||
@ -122,7 +255,7 @@ export const RequestPane: FC<Props> = ({
|
||||
modified: Date.now(),
|
||||
url,
|
||||
parameters,
|
||||
// Hack to force the ui to refresh. More info on use-vcs-version
|
||||
// Hack to force the ui to refresh. More info on use-vcs-version
|
||||
}, true);
|
||||
}
|
||||
}, [request]);
|
||||
@ -159,7 +292,7 @@ export const RequestPane: FC<Props> = ({
|
||||
// Clear saved value in requestMeta
|
||||
await models.requestMeta.update(requestMeta, { savedRequestBody });
|
||||
// @ts-expect-error -- TSCONVERSION mimeType can be null when no body is selected but the updateMimeType logic needs to be reexamined
|
||||
return models.request.updateMimeType(request, mimeType, false, requestMeta.savedRequestBody);
|
||||
return updateMimeType(request, mimeType, false, requestMeta.savedRequestBody);
|
||||
}
|
||||
const numParameters = request.parameters.filter(p => !p.disabled).length;
|
||||
const numHeaders = request.headers.filter(h => !h.disabled).length;
|
||||
|
@ -370,15 +370,6 @@ export const selectActiveCookieJar = createSelector(
|
||||
},
|
||||
);
|
||||
|
||||
export const selectActiveOAuth2Token = createSelector(
|
||||
selectEntitiesLists,
|
||||
selectActiveWorkspaceMeta,
|
||||
(entities, workspaceMeta) => {
|
||||
const id = workspaceMeta?.activeRequestId || 'n/a';
|
||||
return entities.oAuth2Tokens.find(t => t.parentId === id);
|
||||
},
|
||||
);
|
||||
|
||||
export const selectUnseenWorkspaces = createSelector(
|
||||
selectEntitiesLists,
|
||||
entities => {
|
||||
@ -431,7 +422,7 @@ export const selectActiveRequestResponses = createSelector(
|
||||
(activeRequest, entities, activeEnvironment, settings) => {
|
||||
const requestId = activeRequest ? activeRequest._id : 'n/a';
|
||||
|
||||
const responses: (Response | WebSocketResponse)[] = (activeRequest && isWebSocketRequest(activeRequest)) ? entities.webSocketResponses : entities.responses;
|
||||
const responses: (Response | WebSocketResponse)[] = (activeRequest && isWebSocketRequest(activeRequest)) ? entities.webSocketResponses : entities.responses;
|
||||
|
||||
// Filter responses down if the setting is enabled
|
||||
return responses.filter(response => {
|
||||
|
Loading…
Reference in New Issue
Block a user