diff --git a/site/src/routes/auth/_config.js b/site/src/routes/auth/_config.js new file mode 100644 index 0000000000..d155e56de8 --- /dev/null +++ b/site/src/routes/auth/_config.js @@ -0,0 +1,6 @@ +export const oauth = 'https://github.com/login/oauth'; +export const baseurl = process.env.BASEURL; +export const secure = baseurl.startsWith('https:'); + +export const client_id = process.env.GITHUB_CLIENT_ID; +export const client_secret = process.env.GITHUB_CLIENT_SECRET; \ No newline at end of file diff --git a/site/src/routes/auth/callback.js b/site/src/routes/auth/callback.js new file mode 100644 index 0000000000..4635a624ef --- /dev/null +++ b/site/src/routes/auth/callback.js @@ -0,0 +1,68 @@ +import send from '@polka/send'; +import devalue from 'devalue'; +import * as cookie from 'cookie'; +import * as httpie from 'httpie'; +import { parse, stringify } from 'querystring'; +import { find, query } from '../../utils/db.js'; +import { to_user } from '../../utils/auth'; +import { oauth, secure, client_id, client_secret } from './_config.js'; + +export async function get(req, res) { + try { + // Trade "code" for "access_token" + const r1 = await httpie.post(`${oauth}/access_token?` + stringify({ + code: req.query.code, + client_id, + client_secret, + })); + + // Now fetch User details + const { access_token } = parse(r1.data); + const r2 = await httpie.get('https://api.github.com/user', { + headers: { + 'User-Agent': 'svelte.dev', + Authorization: `token ${access_token}` + } + }); + + const { id: uid, name, avatar_url, login } = r2.data; + + // Upsert `users` table + const [user] = await query(` + insert into users(uid, name, username, avatar, github_token) + values ($1, $2, $3, $4, $5) on conflict (uid) do update + set (name, username, avatar, github_token, updated_at) = ($2, $3, $4, $5, now()) + returning * + `, [uid, name, login, avatar_url, access_token]); + + const session = await find(` + insert into sessions(user_id) + values ($1) + returning * + `, [user.id]); + + res.writeHead(200, { + 'Set-Cookie': cookie.serialize('sid', session.uid, { + maxAge: 31536000, + path: '/', + httpOnly: true, + secure + }), + 'Content-Type': 'text/html; charset=utf-8' + }); + + res.end(` + + `); + } catch (err) { + console.error('GET /auth/callback', err); + send(res, 500, err.data, { + 'Content-Type': err.headers['content-type'], + 'Content-Length': err.headers['content-length'] + }); + } +} \ No newline at end of file diff --git a/site/src/routes/auth/login.js b/site/src/routes/auth/login.js new file mode 100644 index 0000000000..7240498418 --- /dev/null +++ b/site/src/routes/auth/login.js @@ -0,0 +1,27 @@ +import send from '@polka/send'; +import { stringify } from 'querystring'; +import { oauth, baseurl, client_id } from './_config.js'; + +export const get = client_id + ? (req, res) => { + const Location = `${oauth}/authorize?` + stringify({ + scope: 'read:user', + client_id, + redirect_uri: `${baseurl}/auth/callback`, + }); + + send(res, 302, Location, { Location }); + } + : (req, res) => { + send(res, 500, ` + +

Missing .env file

+

In order to use GitHub authentication, you will need to register an OAuth application and create a local .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.

+

See also here

+ + `, { + 'Content-Type': 'text/html; charset=utf-8' + }); + }; \ No newline at end of file diff --git a/site/src/routes/auth/logout.js b/site/src/routes/auth/logout.js new file mode 100644 index 0000000000..2a6ec6f8ba --- /dev/null +++ b/site/src/routes/auth/logout.js @@ -0,0 +1,19 @@ +import send from '@polka/send'; +import * as cookie from 'cookie'; +import { query } from '../../utils/db.js'; +import { secure } from './_config.js'; + +export async function get(req, res) { + await query(` + delete from sessions where uid = $1 + `, [req.cookies.sid]); + + send(res, 200, '', { + 'Set-Cookie': cookie.serialize('sid', '', { + maxAge: -1, + path: '/', + httpOnly: true, + secure + }) + }); +} \ No newline at end of file diff --git a/site/src/server.js b/site/src/server.js index ad064366ee..6cca2b17b0 100644 --- a/site/src/server.js +++ b/site/src/server.js @@ -1,21 +1,12 @@ import polka from 'polka'; import send from '@polka/send'; -import devalue from 'devalue'; -import { get, post } from 'httpie'; import sirv from 'sirv'; import * as sapper from '@sapper/server'; import * as cookie from 'cookie'; -import { parse, stringify } from 'querystring'; -import { find, query } from './utils/db'; +import { find } from './utils/db'; +import { to_user } from './utils/auth'; -const { - PORT = 3000, - GITHUB_CLIENT_ID, - GITHUB_CLIENT_SECRET, - BASEURL -} = process.env; - -const OAuth = 'https://github.com/login/oauth'; +const { PORT = 3000 } = process.env; const app = polka({ onError: (err, req, res) => { @@ -25,132 +16,25 @@ const app = polka({ } }); -const to_user = obj => ({ - uid: obj.uid, - username: obj.username, - name: obj.name, - avatar: obj.avatar -}); - -if (GITHUB_CLIENT_ID) { - app.use(async (req, res, next) => { +app.use( + // authenticate user + async (req, res, next) => { if (req.headers.cookie) { - const cookies = cookie.parse(req.headers.cookie); - if (cookies.sid) { - if (req.url === '/auth/logout') { - await query(` - delete from sessions where uid = $1 - `, [cookies.sid]); - send(res, 200); - return; - } - + req.cookies = cookie.parse(req.headers.cookie); + if (req.cookies.sid) { req.user = await find(` select users.id, users.uid, users.username, users.name, users.avatar from sessions left join users on sessions.user_id = users.id where sessions.uid = $1 and expiry > now() - `, [cookies.sid]); + `, [req.cookies.sid]); } } next(); - }); - - app.get('/auth/login', (req, res) => { - try { - 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) { - console.error('GET /auth/login', err); - send(res, 500); - } - }); - - 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: uid, name, avatar_url, login } = r2.data; - - // Upsert `users` table - const [user] = await query(` - insert into users(uid, name, username, avatar, github_token) - values ($1, $2, $3, $4, $5) on conflict (uid) do update - set (name, username, avatar, github_token, updated_at) = ($2, $3, $4, $5, now()) - returning * - `, [uid, name, login, avatar_url, access_token]); - - const session = await find(` - insert into sessions(user_id) - values ($1) - returning * - `, [user.id]); - - const cookie = [ - `sid=${session.uid}`, - `Max-Age=31536000`, - `Path=/`, - `HttpOnly` - ]; - - res.writeHead(200, { - 'Set-Cookie': cookie.join('; '), - 'Content-Type': 'text/html; charset=utf-8' - }); - - res.end(` - - `); - } catch (err) { - console.error('GET /auth/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, ` - -

Missing .env file

-

In order to use GitHub authentication, you will need to register an OAuth application and create a local .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.

-

See also here

- - `, { - 'Content-Type': 'text/html; charset=utf-8' - }); - }); -} - -app.use( + // serve static files sirv('static', { dev: process.env.NODE_ENV === 'development', setHeaders(res) { @@ -159,6 +43,7 @@ app.use( } }), + // run Sapper sapper.middleware({ session: req => ({ user: to_user(req.user) diff --git a/site/src/utils/auth.js b/site/src/utils/auth.js new file mode 100644 index 0000000000..5fc1153735 --- /dev/null +++ b/site/src/utils/auth.js @@ -0,0 +1,6 @@ +export const to_user = obj => obj && ({ + uid: obj.uid, + username: obj.username, + name: obj.name, + avatar: obj.avatar +}); \ No newline at end of file