├── .editorconfig ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── puggle.json ├── src ├── __test__ │ └── index.spec.ts └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # 2 | # Editor config, for sharing IDE preferences ~ https://editorconfig.org/ 3 | # 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | end_of_line = lf 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Files to ignore from git source control 3 | # 4 | 5 | *.env 6 | .DS_Store 7 | node_modules 8 | coverage 9 | dist 10 | package-lock.json 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # 2 | # Files to ignore from npm 3 | # https://npm.github.io/publishing-pkgs-docs/publishing/the-npmignore-file.html 4 | # 5 | 6 | *.env 7 | coverage 8 | 9 | __tests__ 10 | __mocks__ 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # 2 | # Files for prettier to ignore 3 | # 4 | 5 | coverage 6 | node_modules 7 | dist 8 | .nova 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.1 2 | 3 | - Explicitly set the `Content-Type` to be `text/html` to prevent 4 | errors when `X-Content-Type-Options: nosniff` is set. 5 | 6 | Thanks to [Michał Miszczyszyn](https://github.com/mmiszy). 7 | 8 | ## 1.1.0 9 | 10 | - expose `oauthConfig`, `randomState` and `renderResponse` methods 11 | to allow repurposing of the internals for non-vercel contexts. 12 | - add `OAUTH_HOST`, `OAUTH_TOKEN_PATH` and `OAUTH_AUTHORIZE_PATH` 13 | environment variables for more configuration. 14 | 15 | ## 1.0.0 16 | 17 | Initial release 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Newcastle University 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vercel-netlify-cms-github 2 | 3 | An NPM package to allow you to use netlify-cms with GitHub authentication 4 | when deploying on Vercel. 5 | 6 | ## Installation 7 | 8 | **1. Install the module** 9 | 10 | ```bash 11 | # cd into/your/vercel/project 12 | npm install @openlab/vercel-netlify-cms-github 13 | ``` 14 | 15 | **2. Create the auth route** 16 | 17 | Create a vercel endpoint at `api/auth.ts` 18 | 19 | ```ts 20 | export { auth as default } from '@openlab/vercel-netlify-cms-github' 21 | ``` 22 | 23 | **3. Create the callback route** 24 | 25 | Create a vercel endpoint at `api/callback.ts` 26 | 27 | ```ts 28 | export { callback as default } from '@openlab/vercel-netlify-cms-github' 29 | ``` 30 | 31 | **4. Update your config.yml** 32 | 33 | Update your `config.yml` to include this backend 34 | 35 | ```yaml 36 | backend: 37 | name: github 38 | repo: YOUR_GITHUB_REPO 39 | base_url: YOUR_WEBSITE 40 | auth_endpoint: api/auth 41 | ``` 42 | 43 | - **repo** should be your GitHub repo path, like `owner/repo` 44 | - **base_url** should be the full url to the root of your site, like `https://example.com/` 45 | - **auth_endpoint** needs to be set to link it up correctly, you can't put it in `base_url` 46 | 47 | **(optional) Configure vercel.json** 48 | 49 | If you have your admin files in a folder (e.g. `admin/index.html` and `admin/config.yml`) 50 | you might want to force vercel to use trailing slashes. 51 | This is because if you visit `/admin` netlify will look for a config at `/config.yml`, 52 | not in the admin folder. 53 | 54 | To solve this add (or update) your [vercel.json](https://vercel.com/docs/configuration) 55 | in the project root: 56 | 57 | ```json 58 | { 59 | "trailingSlash": true 60 | } 61 | ``` 62 | 63 | **5. Commit these endpoints to git** 64 | 65 | ```bash 66 | git add api/auth.ts api/callback.ts 67 | git commit -m ":star: add GitHub auth routes and connect to netlify-cms" 68 | ``` 69 | 70 | **6. Create a GitHub OAuth application** 71 | 72 | Go to https://github.com/settings/developers. 73 | 74 | - Set **Homepage URL** to your site's homepage 75 | - Set **Authorization callback URL** to `https://YOUR_SITE_HERE/api/callback 76 | - Make a note of your `client_id` and `client_secret` 77 | 78 | **7. Setup Vercel environment variables** 79 | 80 | Go to your vercel dashboard, https://vercel.com. 81 | 82 | - Navigate to your project then **Settings** > **Environment Variables** 83 | - Add `OAUTH_CLIENT_ID` and set the value from the GitHub OAuth application 84 | - Add `OAUTH_CLIENT_SECRET` and set the value from the GitHub OAuth application 85 | - You can store them however you like but secrets should be the most secure 86 | - Make sure your environment variables are exposed on the deployment(s) you need 87 | 88 | **Done** 89 | 90 | 🎉 Your site should now be linked up! 91 | 92 | ## Configuration 93 | 94 | **Environment Variables** 95 | 96 | In addition to `OAUTH_CLIENT_ID` and `OAUTH_CLIENT_SECRET`, 97 | this package also exposes these variables to configure the GitHub authentication. 98 | These all have default values configured to talk to github.com. 99 | 100 | - `OAUTH_HOST` (default: `https://github.com`) 101 | The GitHub server to talk to 102 | - `OAUTH_TOKEN_PATH` (default: `/login/oauth/access_token`) 103 | The path of the GitHub OAuth 104 | [token endpoint](https://datatracker.ietf.org/doc/html/rfc6749#section-3.2) 105 | - `OAUTH_AUTHORIZE_PATH` (default: `/login/oauth/authorize`) 106 | The path of the GitHub OAuth 107 | [Authorization endpoint](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1) 108 | 109 | ## API usage 110 | 111 | Other than the Vercel endpoints, these are exported: 112 | 113 | - `oauthConfig` is an object with configuration for [simple-oauth2](https://www.npmjs.com/package/simple-oauth2) 114 | - `randomState` is a function to generate a random state for an OAuth2 flow 115 | - `renderResponse` is a function to generate HTML with client-side JavaScript 116 | to complete the OAuth2 flow using `window.opener.postMessage` 117 | 118 | > These were primarily exposed for 119 | > [digitalinteraction/netlify-cms-github-auth](http://github.com/digitalinteraction/netlify-cms-github-auth) 120 | > to use. 121 | 122 | --- 123 | 124 | > This project was set up by [puggle](https://npm.im/puggle) 125 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openlab/vercel-netlify-cms-github", 3 | "description": "Vercel API routes to login to netlify-cms deployed on vercel", 4 | "version": "1.1.1", 5 | "repository": "digitalinteraction/vercel-netlify-cms-github", 6 | "author": "", 7 | "license": "MIT", 8 | "scripts": { 9 | "test": "jest", 10 | "coverage": "jest --coverage", 11 | "build": "tsc", 12 | "lint": "tsc --noEmit", 13 | "prettier": "prettier --write '**/*.{js,json,css,md,ts,tsx}'", 14 | "preversion": "npm run test -s && npm run build" 15 | }, 16 | "keywords": [], 17 | "engines": { 18 | "node": ">=12" 19 | }, 20 | "dependencies": { 21 | "dedent": "^0.7.0", 22 | "dotenv": "^8.2.0", 23 | "simple-oauth2": "^4.2.0" 24 | }, 25 | "devDependencies": { 26 | "@types/dedent": "^0.7.0", 27 | "@types/jest": "^26.0.20", 28 | "@types/node": "^14.14.22", 29 | "@types/simple-oauth2": "^4.1.0", 30 | "@vercel/node": "^1.9.0", 31 | "jest": "^26.6.3", 32 | "lint-staged": "^10.5.3", 33 | "nodemon": "^2.0.7", 34 | "prettier": "^2.2.1", 35 | "ts-jest": "^26.4.4", 36 | "ts-node": "^8.10.2", 37 | "typescript": "^3.9.7", 38 | "yorkie": "^2.0.0" 39 | }, 40 | "jest": { 41 | "preset": "ts-jest", 42 | "testEnvironment": "node", 43 | "testPathIgnorePatterns": [ 44 | "/node_modules/", 45 | "/dist/" 46 | ] 47 | }, 48 | "prettier": { 49 | "semi": false, 50 | "singleQuote": true 51 | }, 52 | "gitHooks": { 53 | "pre-commit": "lint-staged" 54 | }, 55 | "lint-staged": { 56 | "*.{js,json,css,md,ts,tsx}": [ 57 | "prettier --write" 58 | ] 59 | }, 60 | "main": "dist/index.js", 61 | "types": "dist/index.d.js" 62 | } 63 | -------------------------------------------------------------------------------- /puggle.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.5.5", 3 | "projectName": "vercel-netlify-cms-github", 4 | "preset": { 5 | "name": "robb-j:ts-node", 6 | "version": "0.2.2" 7 | }, 8 | "plugins": { 9 | "npm": "0.1.0" 10 | }, 11 | "params": { 12 | "npm": { 13 | "packageName": "@openlab/vercel-netlify-cms-github", 14 | "packageInfo": "Vercel API routes to login to netlify-cms deployed on vercel", 15 | "repository": "digitalinteraction/vercel-netlify-cms-github" 16 | }, 17 | "cli": { 18 | "enabled": false 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/__test__/index.spec.ts: -------------------------------------------------------------------------------- 1 | // 2 | // An example unit test 3 | // 4 | 5 | describe('sample', () => { 6 | it('should pass', () => { 7 | expect(1 + 1).toBe(2) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // From https://github.com/robinpokorny/netlify-cms-now 3 | // with the goal of moving to a reusable npm module 4 | // 5 | 6 | import dedent = require('dedent') 7 | import { NowRequest, NowResponse } from '@vercel/node' 8 | import { randomBytes } from 'crypto' 9 | import { AuthorizationCode, ModuleOptions } from 'simple-oauth2' 10 | 11 | const { 12 | OAUTH_CLIENT_ID = '', 13 | OAUTH_CLIENT_SECRET = '', 14 | OAUTH_HOST = 'https://github.com', 15 | OAUTH_TOKEN_PATH = '/login/oauth/access_token', 16 | OAUTH_AUTHORIZE_PATH = '/login/oauth/authorize', 17 | } = process.env 18 | 19 | export const oauthConfig: ModuleOptions = Object.freeze({ 20 | client: Object.freeze({ 21 | id: OAUTH_CLIENT_ID!, 22 | secret: OAUTH_CLIENT_SECRET, 23 | }), 24 | auth: Object.freeze({ 25 | tokenHost: OAUTH_HOST, 26 | tokenPath: OAUTH_TOKEN_PATH, 27 | authorizePath: OAUTH_AUTHORIZE_PATH, 28 | }), 29 | }) 30 | 31 | export function randomState() { 32 | return randomBytes(6).toString('hex') 33 | } 34 | 35 | /** Render a html response with a script to finish a client-side github authentication */ 36 | export function renderResponse(status: 'success' | 'error', content: any) { 37 | return dedent` 38 | 39 | 40 | 41 | 42 | Authorizing ... 43 | 44 | 45 |

46 | 70 | 71 | 72 | ` 73 | } 74 | 75 | /** An endpoint to start an OAuth2 authentication */ 76 | export function auth(req: NowRequest, res: NowResponse) { 77 | const { host } = req.headers 78 | 79 | console.debug('auth host=%o', host) 80 | 81 | const authorizationCode = new AuthorizationCode(oauthConfig) 82 | 83 | const url = authorizationCode.authorizeURL({ 84 | redirect_uri: `https://${host}/api/callback`, 85 | scope: `repo,user`, 86 | state: randomState(), 87 | }) 88 | 89 | res.writeHead(301, { Location: url }) 90 | res.end() 91 | } 92 | 93 | /** An endpoint to finish an OAuth2 authentication */ 94 | export async function callback(req: NowRequest, res: NowResponse) { 95 | try { 96 | const code = req.query.code as string 97 | const { host } = req.headers 98 | 99 | const authorizationCode = new AuthorizationCode(oauthConfig) 100 | 101 | const accessToken = await authorizationCode.getToken({ 102 | code, 103 | redirect_uri: `https://${host}/api/callback`, 104 | }) 105 | 106 | console.debug('callback host=%o', host) 107 | 108 | const { token } = authorizationCode.createToken(accessToken) 109 | 110 | res.setHeader('Content-Type', 'text/html'); 111 | 112 | res.status(200).send( 113 | renderResponse('success', { 114 | token: token.token.access_token, 115 | provider: 'github', 116 | }) 117 | ) 118 | } catch (e) { 119 | res.status(200).send(renderResponse('error', e)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "target": "es2019", // Node.js 12 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "declaration": true, 8 | "newLine": "lf", 9 | "stripInternal": true, 10 | "strict": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noEmitOnError": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "skipLibCheck": true, 16 | "resolveJsonModule": true, 17 | "typeRoots": ["./node_modules/@types", "./src/types"] 18 | }, 19 | "include": ["src"], 20 | "exclude": ["node_modules"] 21 | } 22 | --------------------------------------------------------------------------------