Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AWS Cognito Proxy for Github #7014

Merged
merged 3 commits into from
Jan 30, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/decap-cms-app/package.json
Original file line number Diff line number Diff line change
@@ -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",
2 changes: 2 additions & 0 deletions packages/decap-cms-app/src/extensions.js
Original file line number Diff line number Diff line change
@@ -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);
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
@@ -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 };
@@ -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;
2 changes: 2 additions & 0 deletions packages/decap-cms-backend-gitea/src/AuthenticationPage.js
Original file line number Diff line number Diff line change
@@ -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) => {
20 changes: 12 additions & 8 deletions packages/decap-cms-backend-github/src/API.ts
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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<GitHubUser>;
_userPromise?: Promise<GitHubUser>;
_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<GitHubUser>;
}

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