From 4f419dd27101c4db89d7f227b7a8bd9ac4b52fce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9amus=20=C3=93=20Ceanainn?= Date: Tue, 30 Jan 2024 14:07:27 +0000 Subject: [PATCH] AWS Cognito Proxy for Github (#7014) * feat: support using 'Bearer' keyword instead of 'token' for Github backend * feat: add additional configuration options to PKCE authenticator * feat: add working AWS proxy and update Github and Git Gateway implementations to allow for it --------- Co-authored-by: Seamus O Ceanainn --- packages/decap-cms-app/package.json | 1 + packages/decap-cms-app/src/extensions.js | 2 + .../README.md | 9 +++ .../package.json | 45 +++++++++++ .../src/AuthenticationPage.js | 76 +++++++++++++++++++ .../src/implementation.tsx | 47 ++++++++++++ .../src/index.ts | 12 +++ .../webpack.config.js | 3 + .../src/GitHubAPI.ts | 7 +- .../src/AuthenticationPage.js | 2 + packages/decap-cms-backend-github/src/API.ts | 20 +++-- .../src/GraphQLAPI.ts | 2 +- .../src/implementation.tsx | 26 ++++--- .../src/AuthenticationPage.js | 9 ++- packages/decap-cms-lib-auth/src/pkce-oauth.js | 33 +++++--- .../decap-cms-lib-util/src/implementation.ts | 1 + 16 files changed, 263 insertions(+), 32 deletions(-) create mode 100644 packages/decap-cms-backend-aws-cognito-github-proxy/README.md create mode 100644 packages/decap-cms-backend-aws-cognito-github-proxy/package.json create mode 100644 packages/decap-cms-backend-aws-cognito-github-proxy/src/AuthenticationPage.js create mode 100644 packages/decap-cms-backend-aws-cognito-github-proxy/src/implementation.tsx create mode 100644 packages/decap-cms-backend-aws-cognito-github-proxy/src/index.ts create mode 100644 packages/decap-cms-backend-aws-cognito-github-proxy/webpack.config.js diff --git a/packages/decap-cms-app/package.json b/packages/decap-cms-app/package.json index 1f5413e55595..1f7dcde7f876 100644 --- a/packages/decap-cms-app/package.json +++ b/packages/decap-cms-app/package.json @@ -32,6 +32,7 @@ "dayjs": "^1.11.10", "decap-cms-backend-azure": "^3.1.0-beta.0", "decap-cms-backend-bitbucket": "^3.1.0-beta.0", + "decap-cms-backend-aws-cognito-github-proxy": "^3.1.0-beta.0", "decap-cms-backend-git-gateway": "^3.1.0-beta.0", "decap-cms-backend-github": "^3.1.0-beta.1", "decap-cms-backend-gitlab": "^3.1.0-beta.0", diff --git a/packages/decap-cms-app/src/extensions.js b/packages/decap-cms-app/src/extensions.js index 92fc0bbcbcbc..eef9ebd77063 100644 --- a/packages/decap-cms-app/src/extensions.js +++ b/packages/decap-cms-app/src/extensions.js @@ -2,6 +2,7 @@ import { DecapCmsCore as CMS } from 'decap-cms-core'; // Backends import { AzureBackend } from 'decap-cms-backend-azure'; +import { AwsCognitoGitHubProxyBackend } from 'decap-cms-backend-aws-cognito-github-proxy'; import { GitHubBackend } from 'decap-cms-backend-github'; import { GitLabBackend } from 'decap-cms-backend-gitlab'; import { GiteaBackend } from 'decap-cms-backend-gitea'; @@ -33,6 +34,7 @@ import * as locales from 'decap-cms-locales'; // Register all the things CMS.registerBackend('git-gateway', GitGatewayBackend); CMS.registerBackend('azure', AzureBackend); +CMS.registerBackend('aws-cognito-github-proxy', AwsCognitoGitHubProxyBackend); CMS.registerBackend('github', GitHubBackend); CMS.registerBackend('gitlab', GitLabBackend); CMS.registerBackend('gitea', GiteaBackend); diff --git a/packages/decap-cms-backend-aws-cognito-github-proxy/README.md b/packages/decap-cms-backend-aws-cognito-github-proxy/README.md new file mode 100644 index 000000000000..d8cde5870057 --- /dev/null +++ b/packages/decap-cms-backend-aws-cognito-github-proxy/README.md @@ -0,0 +1,9 @@ +# GitHub backend + +An abstraction layer between the CMS and a proxied version of [Github](https://docs.github.com/en/rest). + +## Code structure + +`Implementation` - wraps [Github Backend](https://github.com/decaporg/decap-cms/tree/master/packages/decap-cms-lib-auth/README.md) for proxied version of Github. + +`AuthenticationPage` - uses [lib-auth](https://github.com/decaporg/decap-cms/tree/master/packages/decap-cms-lib-auth/README.md) to create an AWS Cognito compatible generic Authentication page supporting PKCE. diff --git a/packages/decap-cms-backend-aws-cognito-github-proxy/package.json b/packages/decap-cms-backend-aws-cognito-github-proxy/package.json new file mode 100644 index 000000000000..e7ed4fb41771 --- /dev/null +++ b/packages/decap-cms-backend-aws-cognito-github-proxy/package.json @@ -0,0 +1,45 @@ +{ + "name": "decap-cms-backend-aws-cognito-github-proxy", + "description": "GitHub backend for Decap CMS proxied through AWS Cognito", + "version": "3.1.0-beta.1", + "license": "MIT", + "repository": "https://github.com/decaporg/decap-cms/tree/master/packages/decap-cms-backend-aws-cognito-github-proxy", + "bugs": "https://github.com/decaporg/decap-cms/issues", + "module": "dist/esm/index.js", + "main": "dist/decap-cms-backend-aws-cognito-github-proxy.js", + "keywords": [ + "decap-cms", + "backend", + "github", + "aws-cognito" + ], + "sideEffects": false, + "scripts": { + "develop": "yarn build:esm --watch", + "build": "cross-env NODE_ENV=production webpack", + "build:esm": "cross-env NODE_ENV=esm babel src --out-dir dist/esm --ignore \"**/__tests__\" --root-mode upward --extensions \".js,.jsx,.ts,.tsx\"", + "createFragmentTypes": "node scripts/createFragmentTypes.js" + }, + "dependencies": { + "apollo-cache-inmemory": "^1.6.2", + "apollo-client": "^2.6.3", + "apollo-link-context": "^1.0.18", + "apollo-link-http": "^1.5.15", + "common-tags": "^1.8.0", + "graphql": "^15.0.0", + "graphql-tag": "^2.10.1", + "js-base64": "^3.0.0", + "semaphore": "^1.1.0" + }, + "peerDependencies": { + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "decap-cms-lib-auth": "^3.0.0", + "decap-cms-backend-github": "^3.0.0", + "decap-cms-lib-util": "^3.0.0", + "decap-cms-ui-default": "^3.0.0", + "lodash": "^4.17.11", + "prop-types": "^15.7.2", + "react": "^18.2.0" + } +} diff --git a/packages/decap-cms-backend-aws-cognito-github-proxy/src/AuthenticationPage.js b/packages/decap-cms-backend-aws-cognito-github-proxy/src/AuthenticationPage.js new file mode 100644 index 000000000000..94ce2974fe6a --- /dev/null +++ b/packages/decap-cms-backend-aws-cognito-github-proxy/src/AuthenticationPage.js @@ -0,0 +1,76 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from '@emotion/styled'; +import { PkceAuthenticator } from 'decap-cms-lib-auth'; +import { AuthenticationPage, Icon } from 'decap-cms-ui-default'; + +const LoginButtonIcon = styled(Icon)` + margin-right: 18px; +`; + +export default class GenericPKCEAuthenticationPage extends React.Component { + static propTypes = { + inProgress: PropTypes.bool, + config: PropTypes.object.isRequired, + onLogin: PropTypes.func.isRequired, + t: PropTypes.func.isRequired, + }; + + state = {}; + + componentDidMount() { + const { + base_url = '', + app_id = '', + auth_endpoint = 'oauth2/authorize', + auth_token_endpoint = 'oauth2/token', + redirect_uri = document.location.origin + document.location.pathname, + } = this.props.config.backend; + this.auth = new PkceAuthenticator({ + base_url, + auth_endpoint, + app_id, + auth_token_endpoint, + redirect_uri, + auth_token_endpoint_content_type: 'application/x-www-form-urlencoded; charset=utf-8', + }); + // Complete authentication if we were redirected back to from the provider. + this.auth.completeAuth((err, data) => { + if (err) { + this.setState({ loginError: err.toString() }); + return; + } + this.props.onLogin(data); + }); + } + + handleLogin = e => { + e.preventDefault(); + this.auth.authenticate({ scope: 'https://api.github.com/repo openid email' }, (err, data) => { + if (err) { + this.setState({ loginError: err.toString() }); + return; + } + this.props.onLogin(data); + }); + }; + + render() { + const { inProgress, config, t } = this.props; + return ( + ( + + {inProgress ? t('auth.loggingIn') : t('auth.login')} + + )} + t={t} + /> + ); + } +} diff --git a/packages/decap-cms-backend-aws-cognito-github-proxy/src/implementation.tsx b/packages/decap-cms-backend-aws-cognito-github-proxy/src/implementation.tsx new file mode 100644 index 000000000000..e7bc6de432b8 --- /dev/null +++ b/packages/decap-cms-backend-aws-cognito-github-proxy/src/implementation.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { GitHubBackend } from 'decap-cms-backend-github'; + +import AuthenticationPage from './AuthenticationPage'; + +import type { GitHubUser } from 'decap-cms-backend-github/src/implementation'; +import type { Config } from 'decap-cms-lib-util/src'; + +export default class AwsCognitoGitHubProxyBackend extends GitHubBackend { + constructor(config: Config, options = {}) { + super(config, options); + + this.bypassWriteAccessCheckForAppTokens = true; + this.tokenKeyword = 'Bearer'; + } + + authComponent() { + const wrappedAuthenticationPage = (props: Record) => ( + + ); + wrappedAuthenticationPage.displayName = 'AuthenticationPage'; + return wrappedAuthenticationPage; + } + + async currentUser({ token }: { token: string }): Promise { + if (!this._currentUserPromise) { + this._currentUserPromise = fetch(this.baseUrl + '/oauth2/userInfo', { + headers: { + Authorization: `${this.tokenKeyword} ${token}`, + }, + }).then(async (res: Response): Promise => { + if (res.status == 401) { + this.logout(); + return Promise.reject('Token expired'); + } + const userInfo = await res.json(); + const owner = this.originRepo.split('/')[1]; + return { + name: userInfo.email, + login: owner, + avatar_url: `https://github.com/${owner}.png`, + } as GitHubUser; + }); + } + return this._currentUserPromise; + } +} diff --git a/packages/decap-cms-backend-aws-cognito-github-proxy/src/index.ts b/packages/decap-cms-backend-aws-cognito-github-proxy/src/index.ts new file mode 100644 index 000000000000..1a00e3f445c2 --- /dev/null +++ b/packages/decap-cms-backend-aws-cognito-github-proxy/src/index.ts @@ -0,0 +1,12 @@ +import { API } from 'decap-cms-backend-github'; + +import AwsCognitoGitHubProxyBackend from './implementation'; +import AuthenticationPage from './AuthenticationPage'; + +export const DecapCmsBackendAwsCognitoGithubProxy = { + AwsCognitoGitHubProxyBackend, + API, + AuthenticationPage, +}; + +export { AwsCognitoGitHubProxyBackend, API, AuthenticationPage }; diff --git a/packages/decap-cms-backend-aws-cognito-github-proxy/webpack.config.js b/packages/decap-cms-backend-aws-cognito-github-proxy/webpack.config.js new file mode 100644 index 000000000000..42edd361d4a7 --- /dev/null +++ b/packages/decap-cms-backend-aws-cognito-github-proxy/webpack.config.js @@ -0,0 +1,3 @@ +const { getConfig } = require('../../scripts/webpack.js'); + +module.exports = getConfig(); diff --git a/packages/decap-cms-backend-git-gateway/src/GitHubAPI.ts b/packages/decap-cms-backend-git-gateway/src/GitHubAPI.ts index 10a2bfa11f0f..70cc9f8e2289 100644 --- a/packages/decap-cms-backend-git-gateway/src/GitHubAPI.ts +++ b/packages/decap-cms-backend-git-gateway/src/GitHubAPI.ts @@ -5,7 +5,7 @@ import type { Config as GitHubConfig, Diff } from 'decap-cms-backend-github/src/ import type { FetchError } from 'decap-cms-lib-util'; import type { Octokit } from '@octokit/rest'; -type Config = GitHubConfig & { +type Config = Omit & { apiRoot: string; tokenPromise: () => Promise; commitAuthor: { name: string }; @@ -18,7 +18,10 @@ export default class API extends GithubAPI { isLargeMedia: (filename: string) => Promise; constructor(config: Config) { - super(config); + super({ + getUser: () => Promise.reject('Never used'), + ...config, + }); this.apiRoot = config.apiRoot; this.tokenPromise = config.tokenPromise; this.commitAuthor = config.commitAuthor; diff --git a/packages/decap-cms-backend-gitea/src/AuthenticationPage.js b/packages/decap-cms-backend-gitea/src/AuthenticationPage.js index c61584bcc8d4..2def4d2ac66f 100644 --- a/packages/decap-cms-backend-gitea/src/AuthenticationPage.js +++ b/packages/decap-cms-backend-gitea/src/AuthenticationPage.js @@ -25,6 +25,8 @@ export default class GiteaAuthenticationPage extends React.Component { auth_endpoint: 'login/oauth/authorize', app_id, auth_token_endpoint: 'login/oauth/access_token', + auth_token_endpoint_content_type: 'application/json; charset=utf-8', + redirect_uri: document.location.origin + document.location.pathname, }); // Complete authentication if we were redirected back to from the provider. this.auth.completeAuth((err, data) => { diff --git a/packages/decap-cms-backend-github/src/API.ts b/packages/decap-cms-backend-github/src/API.ts index 3f05b1bbc91a..a044342c5fca 100644 --- a/packages/decap-cms-backend-github/src/API.ts +++ b/packages/decap-cms-backend-github/src/API.ts @@ -50,6 +50,7 @@ export const MOCK_PULL_REQUEST = -1; export interface Config { apiRoot?: string; token?: string; + tokenKeyword?: string; branch?: string; useOpenAuthoring?: boolean; repo?: string; @@ -57,6 +58,8 @@ export interface Config { squashMerges: boolean; initialWorkflowStatus: string; cmsLabelPrefix: string; + baseUrl?: string; + getUser: ({ token }: { token: string }) => Promise; } interface TreeFile { @@ -173,6 +176,7 @@ let migrationNotified = false; export default class API { apiRoot: string; token: string; + tokenKeyword: string; branch: string; useOpenAuthoring?: boolean; repo: string; @@ -186,7 +190,8 @@ export default class API { mergeMethod: string; initialWorkflowStatus: string; cmsLabelPrefix: string; - + baseUrl?: string; + getUser: ({ token }: { token: string }) => Promise; _userPromise?: Promise; _metadataSemaphore?: Semaphore; @@ -195,6 +200,7 @@ export default class API { constructor(config: Config) { this.apiRoot = config.apiRoot || 'https://api.github.com'; this.token = config.token || ''; + this.tokenKeyword = config.tokenKeyword || 'token'; this.branch = config.branch || 'master'; this.useOpenAuthoring = config.useOpenAuthoring; this.repo = config.repo || ''; @@ -213,21 +219,19 @@ export default class API { this.mergeMethod = config.squashMerges ? 'squash' : 'merge'; this.cmsLabelPrefix = config.cmsLabelPrefix; this.initialWorkflowStatus = config.initialWorkflowStatus; + this.baseUrl = config.baseUrl; + this.getUser = config.getUser; } static DEFAULT_COMMIT_MESSAGE = 'Automatically generated by Decap CMS'; user(): Promise<{ name: string; login: string }> { if (!this._userPromise) { - this._userPromise = this.getUser(); + this._userPromise = this.getUser({ token: this.token }); } return this._userPromise; } - getUser() { - return this.request('/user') as Promise; - } - async hasWriteAccess() { try { const result: Octokit.ReposGetResponse = await this.request(this.repoURL); @@ -251,7 +255,7 @@ export default class API { }; if (this.token) { - baseHeader.Authorization = `token ${this.token}`; + baseHeader.Authorization = `${this.tokenKeyword} ${this.token}`; return Promise.resolve(baseHeader); } @@ -576,7 +580,7 @@ export default class API { } try { - const user: GitHubUser = await this.request(`/users/${pullRequest.user.login}`); + const user = await this.user(); return user.name || user.login; } catch { return; diff --git a/packages/decap-cms-backend-github/src/GraphQLAPI.ts b/packages/decap-cms-backend-github/src/GraphQLAPI.ts index 00e1cceb7741..74ab3f172b01 100644 --- a/packages/decap-cms-backend-github/src/GraphQLAPI.ts +++ b/packages/decap-cms-backend-github/src/GraphQLAPI.ts @@ -108,7 +108,7 @@ export default class GraphQLAPI extends API { headers: { 'Content-Type': 'application/json; charset=utf-8', ...headers, - authorization: this.token ? `token ${this.token}` : '', + authorization: this.token ? `${this.tokenKeyword} ${this.token}` : '', }, }; }); diff --git a/packages/decap-cms-backend-github/src/implementation.tsx b/packages/decap-cms-backend-github/src/implementation.tsx index 5a448e746f82..f5412b974c2d 100644 --- a/packages/decap-cms-backend-github/src/implementation.tsx +++ b/packages/decap-cms-backend-github/src/implementation.tsx @@ -42,7 +42,7 @@ import type { } from 'decap-cms-lib-util'; import type { Semaphore } from 'semaphore'; -type GitHubUser = Octokit.UsersGetAuthenticatedResponse; +export type GitHubUser = Octokit.UsersGetAuthenticatedResponse; const MAX_CONCURRENT_DOWNLOADS = 10; @@ -78,9 +78,12 @@ export default class GitHub implements Implementation { mediaFolder: string; previewContext: string; token: string | null; + tokenKeyword: string; squashMerges: boolean; cmsLabelPrefix: string; useGraphql: boolean; + baseUrl?: string; + bypassWriteAccessCheckForAppTokens = false; _currentUserPromise?: Promise; _userIsOriginMaintainerPromises?: { [key: string]: Promise; @@ -119,6 +122,8 @@ export default class GitHub implements Implementation { this.branch = config.backend.branch?.trim() || 'master'; this.apiRoot = config.backend.api_root || 'https://api.github.com'; this.token = ''; + this.tokenKeyword = 'token'; + this.baseUrl = config.backend.base_url; this.squashMerges = config.backend.squash_merges || false; this.cmsLabelPrefix = config.backend.cms_label_prefix || ''; this.useGraphql = config.backend.use_graphql || false; @@ -153,7 +158,7 @@ export default class GitHub implements Implementation { if (api) { auth = (await this.api - ?.getUser() + ?.getUser({ token: this.token ?? '' }) .then(user => !!user) .catch(e => { console.warn('Failed getting GitHub user', e); @@ -185,7 +190,7 @@ export default class GitHub implements Implementation { let repoExists = false; while (!repoExists) { repoExists = await fetch(`${this.apiRoot}/repos/${repo}`, { - headers: { Authorization: `token ${token}` }, + headers: { Authorization: `${this.tokenKeyword} ${token}` }, }) .then(() => true) .catch(err => { @@ -208,7 +213,7 @@ export default class GitHub implements Implementation { if (!this._currentUserPromise) { this._currentUserPromise = fetch(`${this.apiRoot}/user`, { headers: { - Authorization: `token ${token}`, + Authorization: `${this.tokenKeyword} ${token}`, }, }).then(res => res.json()); } @@ -229,7 +234,7 @@ export default class GitHub implements Implementation { `${this.apiRoot}/repos/${this.originRepo}/collaborators/${username}/permission`, { headers: { - Authorization: `token ${token}`, + Authorization: `${this.tokenKeyword} ${token}`, }, }, ) @@ -246,7 +251,7 @@ export default class GitHub implements Implementation { const repo = await fetch(`${this.apiRoot}/repos/${currentUser.login}/${repoName}`, { method: 'GET', headers: { - Authorization: `token ${token}`, + Authorization: `${this.tokenKeyword} ${token}`, }, }).then(res => res.json()); @@ -294,7 +299,7 @@ export default class GitHub implements Implementation { return fetch(`${this.apiRoot}/repos/${this.repo}/merge-upstream`, { method: 'POST', headers: { - Authorization: `token ${token}`, + Authorization: `${this.tokenKeyword} ${token}`, }, body: JSON.stringify({ branch: this.branch, @@ -306,7 +311,7 @@ export default class GitHub implements Implementation { const fork = await fetch(`${this.apiRoot}/repos/${this.originRepo}/forks`, { method: 'POST', headers: { - Authorization: `token ${token}`, + Authorization: `${this.tokenKeyword} ${token}`, }, }).then(res => res.json()); return this.pollUntilForkExists({ repo: fork.full_name, token }); @@ -318,6 +323,7 @@ export default class GitHub implements Implementation { const apiCtor = this.useGraphql ? GraphQLAPI : API; this.api = new apiCtor({ token: this.token, + tokenKeyword: this.tokenKeyword, branch: this.branch, repo: this.repo, originRepo: this.originRepo, @@ -326,6 +332,8 @@ export default class GitHub implements Implementation { cmsLabelPrefix: this.cmsLabelPrefix, useOpenAuthoring: this.useOpenAuthoring, initialWorkflowStatus: this.options.initialWorkflowStatus, + baseUrl: this.baseUrl, + getUser: this.currentUser, }); const user = await this.api!.user(); const isCollab = await this.api!.hasWriteAccess().catch(error => { @@ -342,7 +350,7 @@ export default class GitHub implements Implementation { }); // Unauthorized user - if (!isCollab) { + if (!isCollab && !this.bypassWriteAccessCheckForAppTokens) { throw new Error('Your GitHub user account does not have access to this repo.'); } diff --git a/packages/decap-cms-backend-gitlab/src/AuthenticationPage.js b/packages/decap-cms-backend-gitlab/src/AuthenticationPage.js index 54a097cd5552..6eb5cc61fb59 100644 --- a/packages/decap-cms-backend-gitlab/src/AuthenticationPage.js +++ b/packages/decap-cms-backend-gitlab/src/AuthenticationPage.js @@ -14,7 +14,14 @@ const LoginButtonIcon = styled(Icon)` const clientSideAuthenticators = { pkce: ({ base_url, auth_endpoint, app_id, auth_token_endpoint }) => - new PkceAuthenticator({ base_url, auth_endpoint, app_id, auth_token_endpoint }), + new PkceAuthenticator({ + base_url, + auth_endpoint, + app_id, + auth_token_endpoint, + auth_token_endpoint_content_type: 'application/json; charset=utf-8', + redirect_uri: document.location.origin + document.location.pathname, + }), implicit: ({ base_url, auth_endpoint, app_id, clearHash }) => new ImplicitAuthenticator({ base_url, auth_endpoint, app_id, clearHash }), diff --git a/packages/decap-cms-lib-auth/src/pkce-oauth.js b/packages/decap-cms-lib-auth/src/pkce-oauth.js index 5c1a9ce65e4a..803ce11aebc7 100644 --- a/packages/decap-cms-lib-auth/src/pkce-oauth.js +++ b/packages/decap-cms-lib-auth/src/pkce-oauth.js @@ -53,6 +53,8 @@ export default class PkceAuthenticator { const authTokenEndpoint = trim(config.auth_token_endpoint, '/'); this.auth_url = `${baseURL}/${authEndpoint}`; this.auth_token_url = `${baseURL}/${authTokenEndpoint}`; + this.auth_token_endpoint_content_type = config.auth_token_endpoint_content_type; + this.redirect_uri = trim(config.redirect_uri, '/'); this.appID = config.app_id; } @@ -63,7 +65,7 @@ export default class PkceAuthenticator { const authURL = new URL(this.auth_url); authURL.searchParams.set('client_id', this.appID); - authURL.searchParams.set('redirect_uri', document.location.origin + document.location.pathname); + authURL.searchParams.set('redirect_uri', this.redirect_uri); authURL.searchParams.set('response_type', 'code'); authURL.searchParams.set('scope', options.scope); @@ -92,7 +94,13 @@ export default class PkceAuthenticator { return; } - const { nonce } = JSON.parse(params.get('state')); + let nonce; + try { + nonce = JSON.parse(params.get('state')).nonce; + } catch (SyntaxError) { + nonce = JSON.parse(params.get('state').replace(/\\"/g, '"')).nonce; + } + const validNonce = validateNonce(nonce); if (!validNonce) { return cb(new Error('Invalid nonce')); @@ -106,24 +114,27 @@ export default class PkceAuthenticator { const code = params.get('code'); const authURL = new URL(this.auth_token_url); + const token_request_body_object = { + client_id: this.appID, + code, + grant_type: 'authorization_code', + redirect_uri: this.redirect_uri, + code_verifier: getCodeVerifier(), + }; + const response = await fetch(authURL.href, { method: 'POST', - body: JSON.stringify({ - client_id: this.appID, - code, - grant_type: 'authorization_code', - redirect_uri: document.location.origin + document.location.pathname, - code_verifier: getCodeVerifier(), - }), + body: this.auth_token_endpoint_content_type.startsWith('application/x-www-form-urlencoded') + ? new URLSearchParams(Object.entries(token_request_body_object)).toString() + : JSON.stringify(token_request_body_object), headers: { - 'Content-Type': 'application/json; charset=utf-8', + 'Content-Type': this.auth_token_endpoint_content_type, }, }); const data = await response.json(); //no need for verifier code so remove clearCodeVerifier(); - cb(null, { token: data.access_token, ...data }); } } diff --git a/packages/decap-cms-lib-util/src/implementation.ts b/packages/decap-cms-lib-util/src/implementation.ts index de055099b77c..8b8a05cfe0a0 100644 --- a/packages/decap-cms-lib-util/src/implementation.ts +++ b/packages/decap-cms-lib-util/src/implementation.ts @@ -111,6 +111,7 @@ export type Config = { proxy_url?: string; auth_type?: string; app_id?: string; + base_url?: string; cms_label_prefix?: string; api_version?: string; };