diff --git a/site/.env.example b/site/.env.example index b587a81f72..c59f6f7272 100644 --- a/site/.env.example +++ b/site/.env.example @@ -6,3 +6,7 @@ DATABASE_URL= GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= MAPBOX_ACCESS_TOKEN= + +JWT_EXP=30d +JWT_ALG=HS512 +JWT_KEY= diff --git a/site/package.json b/site/package.json index 689ef58b21..7ca063c3bc 100644 --- a/site/package.json +++ b/site/package.json @@ -15,10 +15,13 @@ "testsrc": "mocha -r esm test/**" }, "dependencies": { + "@polka/parse": "^1.0.0-next.1", "@polka/send": "^1.0.0-next.2", "devalue": "^1.1.0", "do-not-zip": "^1.0.0", "golden-fleece": "^1.0.9", + "httpie": "^1.1.1", + "jsonwebtoken": "^8.5.1", "limax": "^1.7.0", "marked": "^0.6.1", "pg": "^7.10.0", diff --git a/site/src/backend/auth.js b/site/src/backend/auth.js new file mode 100644 index 0000000000..787cd66b4b --- /dev/null +++ b/site/src/backend/auth.js @@ -0,0 +1,149 @@ +import polka from 'polka'; +import devalue from 'devalue'; +import send from '@polka/send'; +import { get, post } from 'httpie'; +import { json } from '@polka/parse'; +import { parse, stringify } from 'querystring'; +import { decode, sign, verify } from './token'; +import { find, query } from '../utils/db'; + +const { + BASEURL, + GITHUB_CLIENT_ID, + GITHUB_CLIENT_SECRET, +} = process.env; + +const OAuth = 'https://github.com/login/oauth'; + +function exit(res, code, msg='') { + send(res, code, msg.charAt(0).toUpperCase() + msg.substring(1)); +} + +function onError(err, req, res) { + const code = err.code || err.status || 500; + res.headersSent || send(res, code, err.message || err); +} + +/** + * Middleware to determine User validity + */ +export async function isUser(req, res) { + const abort = exit.bind(null, res, 401); + + const auth = req.headers.authorization; + if (!auth) return abort('Missing Authorization header'); + + const [scheme, token] = auth.split(' '); + if (scheme !== 'Bearer' || !token) return abort('Invalid Authorization format'); + + let data; + const decoded = decode(token, { complete:true }); + if (!decoded || !decoded.header) return abort('Invalid token'); + + try { + data = await verify(token); + } catch (err) { + return abort(err.message); + } + + const { uid, username } = data; + if (!uid || !username) return abort('Invalid token payload'); + + try { + const row = await find(`select * from users where uid = $1 and username = $2 limit 1`, [uid, username]); + return row || abort('Invalid token'); + } catch (err) { + console.error('Auth.isUser', err); + return send(res, 500, 'Unknown error occurred'); + } +} + +export function toUser(obj={}) { + const { uid, username, name:displayName, avatar } = obj; + const token = sign({ uid, username }); + return { uid, username, displayName, avatar, token }; +} + +export function API() { + const app = polka({ onError }).use(json()); + + if (GITHUB_CLIENT_ID) { + app.get('/auth/login', (req, res) => { + try { + console.log('inside'); + + const Location = `${OAuth}/authorize?` + stringify({ + scope: 'read:user', + client_id: GITHUB_CLIENT_ID, + redirect_uri: `${BASEURL}/auth/callback`, + }); + + send(res, 302, Location, { Location }); + } catch (err) { + // + } + }); + + app.get('/auth/callback', async (req, res) => { + try { + // Trade "code" for "access_token" + const r1 = await post(`${OAuth}/access_token?` + stringify({ + code: req.query.code, + client_id: GITHUB_CLIENT_ID, + client_secret: GITHUB_CLIENT_SECRET, + })); + + // Now fetch User details + const { access_token } = parse(r1.data); + const r2 = await get('https://api.github.com/user', { + headers: { + 'User-Agent': 'svelte.dev', + Authorization: `token ${access_token}` + } + }); + + const { id, name, avatar_url, login } = r2.data; + + // Upsert `users` table + const [user] = await query(` + insert into users(uid, name, username, avatar, token) + values ($1, $2, $3, $4, $5) on conflict (uid) do update + set (name, username, avatar, token) = ($2, $3, $4, $5) + returning * + `, [id, name, login, avatar_url, access_token]); + + send(res, 200, ` + + `, { + 'Content-Type': 'text/html; charset=utf-8' + }); + } catch (err) { + console.error('OAuth Callback', err); + send(res, 500, err.data, { + 'Content-Type': err.headers['content-type'], + 'Content-Length': err.headers['content-length'] + }); + } + }); + } else { + // Print "Misconfigured" error + app.get('/auth/login', (req, res) => { + send(res, 500, ` +
+In order to use GitHub authentication, you will need to register an OAuth application with gist
and read:user
scopes, and create a .env file:
GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000+
The BASEURL
variable should match the callback URL specified for your app.
In order to use GitHub authentication, you will need to register an OAuth application with gist
and read:user
scopes, and create a .env file:
GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000- -
The BASEURL
variable should match the callback URL specified for your app.