diff --git a/packages/insomnia/src/common/har.ts b/packages/insomnia/src/common/har.ts index 55cc114b2..f2ed27257 100644 --- a/packages/insomnia/src/common/har.ts +++ b/packages/insomnia/src/common/har.ts @@ -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; diff --git a/packages/insomnia/src/models/__tests__/request.test.ts b/packages/insomnia/src/models/__tests__/request.test.ts index 8e2f06406..8c08558a7 100644 --- a/packages/insomnia/src/models/__tests__/request.test.ts +++ b/packages/insomnia/src/models/__tests__/request.test.ts @@ -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'); }); }); diff --git a/packages/insomnia/src/models/request.ts b/packages/insomnia/src/models/request.ts index 749a9928b..e9c261742 100644 --- a/packages/insomnia/src/models/request.ts +++ b/packages/insomnia/src/models/request.ts @@ -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; - +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) { return db.docUpdate(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 = {}) { // 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; diff --git a/packages/insomnia/src/models/response.ts b/packages/insomnia/src/models/response.ts index 261c3de63..b0ec49d3e 100644 --- a/packages/insomnia/src/models/response.ts +++ b/packages/insomnia/src/models/response.ts @@ -157,7 +157,7 @@ export async function getLatestForRequest( return response || null; } -export async function create(patch: Record = {}, maxResponses = 20) { +export async function create(patch: Partial = {}, maxResponses = 20): Promise { if (!patch.parentId) { throw new Error('New Response missing `parentId`'); } @@ -168,17 +168,12 @@ export async function create(patch: Record = {}, 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 = { + 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(type, query, Math.max(1, maxResponses)); const recentIds = allResponses.map(r => r._id); diff --git a/packages/insomnia/src/network/authentication.ts b/packages/insomnia/src/network/authentication.ts index c187b39b2..4ff898326 100644 --- a/packages/insomnia/src/network/authentication.ts +++ b/packages/insomnia/src/network/authentication.ts @@ -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; diff --git a/packages/insomnia/src/network/o-auth-2/__tests__/grant-authorization-code.test.ts b/packages/insomnia/src/network/o-auth-2/__tests__/grant-authorization-code.test.ts deleted file mode 100644 index ec8e543b9..000000000 --- a/packages/insomnia/src/network/o-auth-2/__tests__/grant-authorization-code.test.ts +++ /dev/null @@ -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_/), - }); - }); -}); diff --git a/packages/insomnia/src/network/o-auth-2/__tests__/grant-client-credentials.test.ts b/packages/insomnia/src/network/o-auth-2/__tests__/grant-client-credentials.test.ts deleted file mode 100644 index e3841531e..000000000 --- a/packages/insomnia/src/network/o-auth-2/__tests__/grant-client-credentials.test.ts +++ /dev/null @@ -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_/), - }); - }); -}); diff --git a/packages/insomnia/src/network/o-auth-2/__tests__/grant-implicit.test.ts b/packages/insomnia/src/network/o-auth-2/__tests__/grant-implicit.test.ts deleted file mode 100644 index eda27f1ba..000000000 --- a/packages/insomnia/src/network/o-auth-2/__tests__/grant-implicit.test.ts +++ /dev/null @@ -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, - }); - }); -}); diff --git a/packages/insomnia/src/network/o-auth-2/__tests__/grant-password.test.ts b/packages/insomnia/src/network/o-auth-2/__tests__/grant-password.test.ts deleted file mode 100644 index 4102ae11e..000000000 --- a/packages/insomnia/src/network/o-auth-2/__tests__/grant-password.test.ts +++ /dev/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_/), - }); - }); -}); diff --git a/packages/insomnia/src/network/o-auth-2/__tests__/misc.test.ts b/packages/insomnia/src/network/o-auth-2/__tests__/misc.test.ts index c4faba075..19d5ef063 100644 --- a/packages/insomnia/src/network/o-auth-2/__tests__/misc.test.ts +++ b/packages/insomnia/src/network/o-auth-2/__tests__/misc.test.ts @@ -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); diff --git a/packages/insomnia/src/network/o-auth-2/constants.ts b/packages/insomnia/src/network/o-auth-2/constants.ts index 5789ee701..130bf3581 100644 --- a/packages/insomnia/src/network/o-auth-2/constants.ts +++ b/packages/insomnia/src/network/o-auth-2/constants.ts @@ -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'; diff --git a/packages/insomnia/src/network/o-auth-2/get-token.ts b/packages/insomnia/src/network/o-auth-2/get-token.ts index d9602911b..13c90bcee 100644 --- a/packages/insomnia/src/network/o-auth-2/get-token.ts +++ b/packages/insomnia/src/network/o-auth-2/get-token.ts @@ -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 { - 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 { - const oAuth2Token = await _getAccessToken(requestId, authentication, forceRefresh); - +): Promise => { + 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 { - 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 { - 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 { - 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 { - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // - // 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, -): Promise { - 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>): Partial => { + 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 | 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 }] : []; diff --git a/packages/insomnia/src/network/o-auth-2/grant-authorization-code.ts b/packages/insomnia/src/network/o-auth-2/grant-authorization-code.ts deleted file mode 100644 index 1802e5064..000000000 --- a/packages/insomnia/src/network/o-auth-2/grant-authorization-code.ts +++ /dev/null @@ -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> { - 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> { - 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, ''); -} diff --git a/packages/insomnia/src/network/o-auth-2/grant-client-credentials.ts b/packages/insomnia/src/network/o-auth-2/grant-client-credentials.ts deleted file mode 100644 index 2f9d4a307..000000000 --- a/packages/insomnia/src/network/o-auth-2/grant-client-credentials.ts +++ /dev/null @@ -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> { - 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; -} diff --git a/packages/insomnia/src/network/o-auth-2/grant-implicit.ts b/packages/insomnia/src/network/o-auth-2/grant-implicit.ts deleted file mode 100644 index b8d91fe5d..000000000 --- a/packages/insomnia/src/network/o-auth-2/grant-implicit.ts +++ /dev/null @@ -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> { - 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 {}; - } -} diff --git a/packages/insomnia/src/network/o-auth-2/grant-password.ts b/packages/insomnia/src/network/o-auth-2/grant-password.ts deleted file mode 100644 index df82368fd..000000000 --- a/packages/insomnia/src/network/o-auth-2/grant-password.ts +++ /dev/null @@ -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> { - 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; -} diff --git a/packages/insomnia/src/network/o-auth-2/misc.ts b/packages/insomnia/src/network/o-auth-2/misc.ts index de51e6c49..d53c8bea9 100644 --- a/packages/insomnia/src/network/o-auth-2/misc.ts +++ b/packages/insomnia/src/network/o-auth-2/misc.ts @@ -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 = {}) { - 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 = {}; - - 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=).*/, diff --git a/packages/insomnia/src/network/o-auth-2/refresh-token.ts b/packages/insomnia/src/network/o-auth-2/refresh-token.ts deleted file mode 100644 index e4fbd7e38..000000000 --- a/packages/insomnia/src/network/o-auth-2/refresh-token.ts +++ /dev/null @@ -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> { - 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, - }, - ); -} diff --git a/packages/insomnia/src/ui/components/editors/auth/o-auth-2-auth.tsx b/packages/insomnia/src/ui/components/editors/auth/o-auth-2-auth.tsx index 42cb8d597..28bc456c3 100644 --- a/packages/insomnia/src/ui/components/editors/auth/o-auth-2-auth.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/o-auth-2-auth.tsx @@ -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 = ; - const accessTokenUrl = ; + const accessTokenUrl = ; const redirectUri = ; const state = ; const scope = ; const username = ; - const password = ; + const password = ; const tokenPrefix = ; const responseType = { {advanced} { - +
) : null}