Skip to content

Commit

Permalink
AWS Cognito Proxy for Github (#7014)
Browse files Browse the repository at this point in the history
* 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 <seamus@whatnot.com>
  • Loading branch information
soceanainn and seamuswn committed Jan 30, 2024
1 parent debab39 commit 4f419dd
Show file tree
Hide file tree
Showing 16 changed files with 263 additions and 32 deletions.
1 change: 1 addition & 0 deletions packages/decap-cms-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/decap-cms-app/src/extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions packages/decap-cms-backend-aws-cognito-github-proxy/README.md
Original file line number Diff line number Diff line change
@@ -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.
45 changes: 45 additions & 0 deletions packages/decap-cms-backend-aws-cognito-github-proxy/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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 (
<AuthenticationPage
onLogin={this.handleLogin}
loginDisabled={inProgress}
loginErrorMessage={this.state.loginError}
logoUrl={config.logo_url}
siteUrl={config.site_url}
renderButtonContent={() => (
<React.Fragment>
<LoginButtonIcon type="link" /> {inProgress ? t('auth.loggingIn') : t('auth.login')}
</React.Fragment>
)}
t={t}
/>
);
}
}
Original file line number Diff line number Diff line change
@@ -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<string, unknown>) => (
<AuthenticationPage {...props} backend={this} />
);
wrappedAuthenticationPage.displayName = 'AuthenticationPage';
return wrappedAuthenticationPage;
}

async currentUser({ token }: { token: string }): Promise<GitHubUser> {
if (!this._currentUserPromise) {
this._currentUserPromise = fetch(this.baseUrl + '/oauth2/userInfo', {
headers: {
Authorization: `${this.tokenKeyword} ${token}`,
},
}).then(async (res: Response): Promise<GitHubUser> => {
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;
}
}
12 changes: 12 additions & 0 deletions packages/decap-cms-backend-aws-cognito-github-proxy/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { getConfig } = require('../../scripts/webpack.js');

module.exports = getConfig();
7 changes: 5 additions & 2 deletions packages/decap-cms-backend-git-gateway/src/GitHubAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GitHubConfig, 'getUser'> & {
apiRoot: string;
tokenPromise: () => Promise<string>;
commitAuthor: { name: string };
Expand All @@ -18,7 +18,10 @@ export default class API extends GithubAPI {
isLargeMedia: (filename: string) => Promise<boolean>;

constructor(config: Config) {
super(config);
super({
getUser: () => Promise.reject('Never used'),
...config,
});
this.apiRoot = config.apiRoot;
this.tokenPromise = config.tokenPromise;
this.commitAuthor = config.commitAuthor;
Expand Down
2 changes: 2 additions & 0 deletions packages/decap-cms-backend-gitea/src/AuthenticationPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
20 changes: 12 additions & 8 deletions packages/decap-cms-backend-github/src/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,16 @@ export const MOCK_PULL_REQUEST = -1;
export interface Config {
apiRoot?: string;
token?: string;
tokenKeyword?: string;
branch?: string;
useOpenAuthoring?: boolean;
repo?: string;
originRepo?: string;
squashMerges: boolean;
initialWorkflowStatus: string;
cmsLabelPrefix: string;
baseUrl?: string;
getUser: ({ token }: { token: string }) => Promise<GitHubUser>;
}

interface TreeFile {
Expand Down Expand Up @@ -173,6 +176,7 @@ let migrationNotified = false;
export default class API {
apiRoot: string;
token: string;
tokenKeyword: string;
branch: string;
useOpenAuthoring?: boolean;
repo: string;
Expand All @@ -186,7 +190,8 @@ export default class API {
mergeMethod: string;
initialWorkflowStatus: string;
cmsLabelPrefix: string;

baseUrl?: string;
getUser: ({ token }: { token: string }) => Promise<GitHubUser>;
_userPromise?: Promise<GitHubUser>;
_metadataSemaphore?: Semaphore;

Expand All @@ -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 || '';
Expand All @@ -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<GitHubUser>;
}

async hasWriteAccess() {
try {
const result: Octokit.ReposGetResponse = await this.request(this.repoURL);
Expand All @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/decap-cms-backend-github/src/GraphQLAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}` : '',
},
};
});
Expand Down
Loading

0 comments on commit 4f419dd

Please sign in to comment.