diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..24de1f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [1.0.0] - 2017-04-01 +### Added +- Support for state parameter to protect against cross-site request forgery attacks. Requires users to use the provided `/login` url and moves redirect url from `/` to `/callback` diff --git a/README.md b/README.md index d7a6e60..925a800 100644 --- a/README.md +++ b/README.md @@ -28,17 +28,17 @@ GH_HOST=github.my-company.com > Create an application on GitHub [here](https://github.com/settings/applications/new) to get your client id and secret if you haven't done that already. -When authentication was successful, the user will be redirected to the `REDIRECT_URL` with the `access_token` query param set to the GitHub access token. You can then use that token to interact with the [GitHub API](https://developer.github.com/v3/)! +When authentication is successful, the user will be redirected to the `REDIRECT_URL` with the `access_token` query param set to the GitHub access token. You can then use that token to interact with the [GitHub API](https://developer.github.com/v3/)! > E.g. setting `REDIRECT_URL=https://google.com` will redirect them to `https://google.com/?access_token=asdf123`. (where `asdf123` is the provided access token) ### Finish setup -To make this work you have to set the authorization callback URL of [your application on GitHub](https://github.com/settings/developers) to whatever URL `now` gave you: +To make this work you have to set the authorization callback URL of [your application on GitHub](https://github.com/settings/developers) to whatever URL `now` gave you plus the path `/callback` e.g. `http://localhost:3000/callback`: -![Authorization callback URL: 'your-url.now.sh'](https://cloud.githubusercontent.com/assets/7525670/22621592/95546272-eb27-11e6-80f3-6a2cd556d319.png) +![Authorization callback URL: 'your-url.now.sh'](https://cloud.githubusercontent.com/assets/168870/24585953/9543e03a-178e-11e7-8f10-07be5c10682c.png) -To log people in they just have to click on a link to `https://github.com/login/oauth/authorize?client_id=asdf123`. (where `client_id` is your GitHub app client id) This will redirect them to the GitHub sign in page for your app, which looks like this: +To log people in provide a link to url `now` gave you plus the path `login` e.g. `http://localhost:3000/login` when they click on the link it will redirect to `https://github.com/login/oauth/authorize?client_id=asdf123&state`. (where `client_id` is your GitHub app client id in `.env` and `state` is a randomly generated string). This will redirect them to the GitHub sign in page for your app, which looks like this: ![Authorize my app to access your data on GitHub](https://cloud.githubusercontent.com/assets/7525670/22627265/fc50c680-ebbf-11e6-9126-dcdef37d3c3d.png) @@ -62,7 +62,7 @@ Move `.env.example` to `.env` and fill in your GitHub API details and redirect u npm run dev ``` -The server will then be listening at `localhost:3000`, so set the authorization callback URL of your dev application on GitHub to that. +The server will then be listening at `localhost:3000`, so set the authorization callback URL of your dev application on GitHub to `http://localhost:3000/callback`. ## Updating diff --git a/index.js b/index.js index 3a28fd1..8e0d173 100644 --- a/index.js +++ b/index.js @@ -1,27 +1,43 @@ require('dotenv').config() -const url = require('url') const querystring = require('querystring') const axios = require('axios') -const { send } = require('micro') +const { router, get } = require('microrouter'); +const redirect = require('micro-redirect'); +const uid = require('uid-promise'); + const githubUrl = process.env.GH_HOST || 'github.com' -const createRedirectHTML = (data) => { - const url = `${process.env.REDIRECT_URL}?${querystring.stringify(data)}` - return ` - - -Redirecting… - - -` +const states = []; + +const redirectWithQueryString = (res, data) => { + const location = `${process.env.REDIRECT_URL}?${querystring.stringify(data)}` + redirect(res, 302, location) } -module.exports = async (req, res) => { +const login = async (req, res) => { + const state = await uid(20); + states.push(state); + const { scope, allow_signup } = req.query; + const query = { + client_id: process.env.GH_CLIENT_ID, + state: state + }; + if (scope) query.scope = scope; + if (allow_signup !== undefined) query.allow_signup = allow_signup; + redirect(res, 302, `https://${githubUrl}/login/oauth/authorize?${querystring.stringify(query)}`); +}; + +const callback = async (req, res) => { + res.setHeader('Content-Type', 'text/html') - const { query: { code } } = url.parse(req.url, true) - if (!code) { - send(res, 401, createRedirectHTML({ error: 'Provide code query param' })) + const { code, state } = req.query + + if (!code && !state) { + redirectWithQueryString(res, { error: 'Provide code and state query param' }) + } else if (!states.includes(state)) { + redirectWithQueryString(res, { error: 'Unknown state' }) } else { + states.splice(states.indexOf(state), 1); try { const { status, data } = await axios({ method: 'POST', @@ -37,15 +53,20 @@ module.exports = async (req, res) => { if (status === 200) { const qs = querystring.parse(data) if (qs.error) { - send(res, 401, createRedirectHTML({ error: qs.error_description })) + redirectWithQueryString(res, { error: qs.error_description }) } else { - send(res, 200, createRedirectHTML({ access_token: qs.access_token })) + redirectWithQueryString(res, { access_token: qs.access_token }) } } else { - send(res, 500, createRedirectHTML({ error: 'GitHub server error.' })) + redirectWithQueryString(res, { error: 'GitHub server error.' }) } } catch (err) { - send(res, 500, createRedirectHTML({ error: 'Please provide GH_CLIENT_ID and GH_CLIENT_SECRET as environment variables. (or GitHub might be down)' })) + redirectWithQueryString(res, { error: 'Please provide GH_CLIENT_ID and GH_CLIENT_SECRET as environment variables. (or GitHub might be down)' }) } } } + +module.exports = router( + get('/login', login), + get('/callback', callback) +); diff --git a/package.json b/package.json index 78c9ba1..61bc68a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,10 @@ "dependencies": { "axios": "^0.15.3", "dotenv": "^4.0.0", - "micro": "^6.2.1" + "micro": "^6.2.1", + "micro-redirect": "^1.0.0", + "microrouter": "^2.0.1", + "uid-promise": "^0.1.0" }, "devDependencies": { "nodemon": "^1.11.0",