use cookies instead of JWT

pull/3420/head
Richard Harris 5 years ago
parent 120ee28c4f
commit fb5179b0f3

5
package-lock.json generated

@ -535,6 +535,11 @@
"safe-buffer": "~5.1.1"
}
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",

@ -99,5 +99,7 @@
"sourceMap": true,
"instrument": true
},
"dependencies": {}
"dependencies": {
"cookie": "^0.4.0"
}
}

@ -0,0 +1,15 @@
exports.up = DB => {
DB.sql(`
create table if not exists sessions (
uid uuid NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
user_id integer REFERENCES users(id) not null,
expiry timestamp without time zone DEFAULT now() + interval '1 year'
);
`);
};
exports.down = DB => {
DB.sql(`
drop table if exists sessions;
`);
};

@ -3840,6 +3840,11 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",

@ -24,6 +24,7 @@
"polka": "^1.0.0-next.4",
"prismjs": "^1.17.1",
"sirv": "^0.4.2",
"uuid": "^3.3.2",
"yootils": "0.0.16"
},
"devDependencies": {

@ -1,150 +0,0 @@
import polka from 'polka';
import devalue from 'devalue';
import send from '@polka/send';
import { get, post } from 'httpie';
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='') {
const error = msg.charAt(0).toUpperCase() + msg.substring(1);
send(res, code, { error });
}
function onError(err, req, res) {
const error = err.message || err;
const code = err.code || err.status || 500;
res.headersSent || send(res, code, { error });
}
/**
* 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, avatar } = obj;
const token = sign({ uid, username });
return { uid, username, name, avatar, token };
}
export function API() {
const app = polka({ onError });
if (GITHUB_CLIENT_ID) {
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, 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 *
`, [id, name, login, avatar_url, access_token]);
send(res, 200, `
<script>
window.opener.postMessage({
user: ${devalue(toUser(user))}
}, window.location.origin);
</script>
`, {
'Content-Type': 'text/html; charset=utf-8'
});
} 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, `
<body style="font-family: sans-serif; background: rgb(255,215,215); border: 2px solid red; margin: 0; padding: 1em;">
<h1>Missing .env file</h1>
<p>In order to use GitHub authentication, you will need to <a target="_blank" href="https://github.com/settings/developers">register an OAuth application</a> and create a local .env file:</p>
<pre>GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000</pre>
<p>The <code>BASEURL</code> variable should match the callback URL specified for your app.</p>
<p>See also <a target="_blank" href="https://github.com/sveltejs/svelte/tree/master/site#repl-github-integration">here</a></p>
</body>
`, {
'Content-Type': 'text/html; charset=utf-8'
});
});
}
return app;
}

@ -1,22 +0,0 @@
import * as jwt from 'jsonwebtoken';
const { JWT_KEY, JWT_ALG, JWT_EXP } = process.env;
const CONFIG = {
expiresIn: JWT_EXP,
issuer: 'https://svelte.dev',
audience: 'https://svelte.dev',
algorithm: JWT_ALG,
};
export const decode = jwt.decode;
export function sign(obj, opts, cb) {
opts = Object.assign({}, opts, CONFIG);
return jwt.sign(obj, JWT_KEY, opts, cb);
}
export function verify(str, opts, cb) {
opts = Object.assign({}, opts, CONFIG);
return jwt.verify(str, JWT_KEY, opts, cb);
}

@ -1,8 +0,0 @@
import send from '@polka/send';
import { isUser, toUser } from '../../backend/auth';
export async function get(req, res) {
const user = await isUser(req, res);
res.setHeader('Cache-Control', 'private, no-cache, no-store');
return send(res, 200, user ? toUser(user) : null);
}

@ -1,15 +1,24 @@
<script>
import { user, logout } from '../../../../../user.js';
import { stores } from '@sapper/app';
const { session } = stores();
const logout = async () => {
const r = await fetch(`/auth/logout`, {
credentials: 'include'
});
if (r.ok) $session.user = null;
}
let showMenu = false;
let name;
$: name = $user.name || $user.username;
$: name = $session.user.name || $session.user.username;
</script>
<div class="user" on:mouseenter="{() => showMenu = true}" on:mouseleave="{() => showMenu = false}">
<span>{name}</span>
<img alt="{name} avatar" src="{$user.avatar}">
<img alt="{name} avatar" src="{$session.user.avatar}">
{#if showMenu}
<div class="menu">

@ -1,14 +1,15 @@
<script>
import { createEventDispatcher } from 'svelte';
import { stores } from '@sapper/app';
import UserMenu from './UserMenu.svelte';
import { Icon } from '@sveltejs/site-kit';
import * as doNotZip from 'do-not-zip';
import downloadBlob from '../../../_utils/downloadBlob.js';
import { user } from '../../../../../user.js';
import { enter } from '../../../../../utils/events.js';
import { isMac } from '../../../../../utils/compat.js';
const dispatch = createEventDispatcher();
const { session } = stores();
export let repl;
export let gist;
@ -25,8 +26,7 @@
return new Promise(f => setTimeout(f, ms));
}
$: Authorization = $user && `Bearer ${$user.token}`;
$: canSave = $user && gist && gist.owner === $user.uid;
$: canSave = $session.user && gist && gist.owner === $session.user.uid;
function handleKeydown(event) {
if (event.which === 83 && (isMac ? event.metaKey : event.ctrlKey)) {
@ -41,7 +41,7 @@
const handleLogin = event => {
loginWindow.close();
user.set(event.data.user);
$session.user = event.data.user;
window.removeEventListener('message', handleLogin);
};
@ -56,7 +56,7 @@
try {
const r = await fetch(`repl/create.json`, {
method: 'POST',
headers: { Authorization },
credentials: 'include',
body: JSON.stringify({
name,
files: components.map(component => ({
@ -111,7 +111,7 @@
const r = await fetch(`repl/${gist.uid}.json`, {
method: 'PATCH',
headers: { Authorization },
credentials: 'include',
body: JSON.stringify({
name,
files: components.map(component => ({
@ -199,7 +199,7 @@ export default app;` });
<Icon name="download" />
</button>
<button class="icon" disabled="{saving || !$user}" on:click={() => fork(false)} title="fork">
<button class="icon" disabled="{saving || !$session.user}" on:click={() => fork(false)} title="fork">
{#if justForked}
<Icon name="check" />
{:else}
@ -207,7 +207,7 @@ export default app;` });
{/if}
</button>
<button class="icon" disabled="{saving || !$user}" on:click={save} title="save">
<button class="icon" disabled="{saving || !$session.user}" on:click={save} title="save">
{#if justSaved}
<Icon name="check" />
{:else}
@ -215,8 +215,8 @@ export default app;` });
{/if}
</button>
{#if $user}
<UserMenu />
{#if $session.user}
<UserMenu/>
{:else}
<button class="icon" on:click={login}>
<Icon name="log-in" />

@ -2,7 +2,6 @@ import send from '@polka/send';
import body from '../_utils/body.js';
import * as httpie from 'httpie';
import { query, find } from '../../../utils/db';
import { isUser } from '../../../backend/auth';
import { get_example } from '../../examples/_examples.js';
const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env;
@ -44,7 +43,7 @@ async function import_gist(req, res) {
});
// add gist to database...
const [gist] = await query(`
await query(`
insert into gists(uid, user_id, name, files)
values ($1, $2, $3, $4) returning *`, [req.params.id, user.id, data.description, JSON.stringify(files)]);
@ -92,10 +91,10 @@ export async function get(req, res) {
}
export async function patch(req, res) {
const user = await isUser(req, res);
if (!user) return; // response already sent
const { user } = req;
if (!user) return;
let id, uid=req.params.id;
let id, uid = req.params.id;
try {
const [row] = await query(`select * from gists where uid = $1 limit 1`, [uid]);

@ -10,14 +10,15 @@
<script>
import Repl from '@sveltejs/svelte-repl';
import { onMount } from 'svelte';
import { goto } from '@sapper/app';
import { user } from '../../../user.js';
import { goto, stores } from '@sapper/app';
import InputOutputToggle from '../../../components/Repl/InputOutputToggle.svelte';
import AppControls from './_components/AppControls/index.svelte';
export let version;
export let id;
const { session } = stores();
let repl;
let gist;
let name = 'Loading...';
@ -107,7 +108,7 @@
$: mobile = width < 540;
$: relaxed = is_relaxed_gist || ($user && gist && $user.uid === gist.owner);
$: relaxed = is_relaxed_gist || ($session.user && gist && $session.user.uid === gist.owner);
</script>
<style>

@ -1,10 +1,9 @@
import send from '@polka/send';
import body from './_utils/body.js';
import { query } from '../../utils/db';
import { isUser } from '../../backend/auth';
export async function post(req, res) {
const user = await isUser(req, res);
const { user } = req;
if (!user) return; // response already sent
try {

@ -1,21 +1,169 @@
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 { API } from './backend/auth';
import * as cookie from 'cookie';
import { parse, stringify } from 'querystring';
import { find, query } from './utils/db';
const { PORT = 3000 } = process.env;
const {
PORT = 3000,
GITHUB_CLIENT_ID,
GITHUB_CLIENT_SECRET,
BASEURL
} = process.env;
API()
.use(
sirv('static', {
dev: process.env.NODE_ENV === 'development',
setHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.hasHeader('Cache-Control') || res.setHeader('Cache-Control', 'max-age=600'); // 10min default
const OAuth = 'https://github.com/login/oauth';
const app = polka({
onError: (err, req, res) => {
const error = err.message || err;
const code = err.code || err.status || 500;
res.headersSent || send(res, code, { error });
}
});
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) => {
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.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]);
}
}),
}
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}`
}
});
sapper.middleware({
//
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(`
<script>
window.opener.postMessage({
user: ${devalue(to_user(user))}
}, window.location.origin);
</script>
`);
} 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, `
<body style="font-family: sans-serif; background: rgb(255,215,215); border: 2px solid red; margin: 0; padding: 1em;">
<h1>Missing .env file</h1>
<p>In order to use GitHub authentication, you will need to <a target="_blank" href="https://github.com/settings/developers">register an OAuth application</a> and create a local .env file:</p>
<pre>GITHUB_CLIENT_ID=[YOUR_APP_ID]\nGITHUB_CLIENT_SECRET=[YOUR_APP_SECRET]\nBASEURL=http://localhost:3000</pre>
<p>The <code>BASEURL</code> variable should match the callback URL specified for your app.</p>
<p>See also <a target="_blank" href="https://github.com/sveltejs/svelte/tree/master/site#repl-github-integration">here</a></p>
</body>
`, {
'Content-Type': 'text/html; charset=utf-8'
});
});
}
app.use(
sirv('static', {
dev: process.env.NODE_ENV === 'development',
setHeaders(res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.hasHeader('Cache-Control') || res.setHeader('Cache-Control', 'max-age=600'); // 10min default
}
}),
sapper.middleware({
session: req => ({
user: to_user(req.user)
})
)
.listen(PORT);
})
);
app.listen(PORT);

@ -1,33 +0,0 @@
import { writable } from 'svelte/store';
export const user = writable(null);
if (process.browser) {
const storageKey = 'svelte-dev:token';
// On load, get the last-known user token (if any)
// Note: We can skip this all by writing User data?
const token = localStorage.getItem(storageKey);
// Write changes to localStorage
user.subscribe(obj => {
if (obj) {
localStorage.setItem(storageKey, obj.token);
} else {
localStorage.removeItem(storageKey);
}
});
if (token) {
// If token, refresh the User data from API
const headers = { Authorization: `Bearer ${token}` };
fetch('/auth/me.json', { headers })
.then(r => r.ok ? r.json() : null)
.then(user.set);
}
}
export function logout() {
user.set(null);
}
Loading…
Cancel
Save