Skip to content

Commit

Permalink
Move CSRF checks to be server-side
Browse files Browse the repository at this point in the history
  • Loading branch information
sylveon committed Feb 27, 2022
1 parent bb2dc3a commit 1a12e02
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 83 deletions.
133 changes: 78 additions & 55 deletions func/oauth-callback.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,98 @@ import fetch from 'node-fetch';
import { getUserInfo, getBan, isBlocked } from "./helpers/user-helpers.js";
import { createJwt } from "./helpers/jwt-helpers.js";

function parseCookies(str) {
return str
.split(';')
.map(v => v.split('='))
.reduce((acc, v) => {
acc[v[0]] = v[1];
return acc;
}, {});
}

function verifyCsrf(event) {
if (event.queryStringParameters.state !== undefined && event.headers.Cookie !== undefined) {
const cookies = parseCookies(event.headers.Cookie);
if (cookies["__Secure-CSRFState"] !== undefined) {
return cookies["__Secure-CSRFState"] === event.queryStringParameters.state;
}
}

return false;
}

export async function handler(event, context) {
if (event.httpMethod !== "GET") {
return {
statusCode: 405
};
}

if (event.queryStringParameters.code !== undefined) {
const result = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
body: new URLSearchParams({
client_id: process.env.DISCORD_CLIENT_ID,
client_secret: process.env.DISCORD_CLIENT_SECRET,
grant_type: "authorization_code",
code: event.queryStringParameters.code,
redirect_uri: new URL(event.path, DEPLOY_PRIME_URL),
scope: "identify"
})
});

const data = await result.json();

if (!result.ok) {
console.log(data);
throw new Error("Failed to get user access token");
}

const user = await getUserInfo(data.access_token);
if (isBlocked(user.id)) {
return {
statusCode: 303,
headers: {
"Location": `/error?msg=${encodeURIComponent("You cannot submit ban appeals with this Discord account.")}`,
},
};
}

if (process.env.GUILD_ID && !process.env.SKIP_BAN_CHECK) {
const ban = await getBan(user.id, process.env.GUILD_ID, process.env.DISCORD_BOT_TOKEN);
if (ban === null) {
if (verifyCsrf(event)) {
if (event.queryStringParameters.code !== undefined) {
const result = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
body: new URLSearchParams({
client_id: process.env.DISCORD_CLIENT_ID,
client_secret: process.env.DISCORD_CLIENT_SECRET,
grant_type: "authorization_code",
code: event.queryStringParameters.code,
redirect_uri: new URL(event.path, DEPLOY_PRIME_URL),
scope: "identify"
})
});

const data = await result.json();

if (!result.ok) {
console.log(data);
throw new Error("Failed to get user access token");
}

const user = await getUserInfo(data.access_token);
if (isBlocked(user.id)) {
return {
statusCode: 303,
headers: {
"Location": "/error"
}
"Location": `/error?msg=${encodeURIComponent("You cannot submit ban appeals with this Discord account.")}`,
},
};
}

if (process.env.GUILD_ID && !process.env.SKIP_BAN_CHECK) {
const ban = await getBan(user.id, process.env.GUILD_ID, process.env.DISCORD_BOT_TOKEN);
if (ban === null) {
return {
statusCode: 303,
headers: {
"Location": "/error"
}
};
}
}

const userPublic = {
id: user.id,
avatar: user.avatar,
username: user.username,
discriminator: user.discriminator
};

return {
statusCode: 303,
headers: {
"Location": `/form?token=${encodeURIComponent(createJwt(userPublic, data.expires_in))}`
}
};
} else {
return {
statusCode: 400
};
}

const userPublic = {
id: user.id,
avatar: user.avatar,
username: user.username,
discriminator: user.discriminator
};
let url = `/form?token=${encodeURIComponent(createJwt(userPublic, data.expires_in))}`;
if (event.queryStringParameters.state !== undefined) {
url += `&state=${encodeURIComponent(event.queryStringParameters.state)}`;
}

} else {
return {
statusCode: 303,
headers: {
"Location": url
}
statusCode: 403
};
}

return {
statusCode: 400
};
}
10 changes: 5 additions & 5 deletions func/oauth.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import crypto from "crypto";

export async function handler(event, context) {
const redirectUri = new URL("/.netlify/functions/oauth-callback", DEPLOY_PRIME_URL);
let url = `https://discord.com/api/oauth2/authorize?client_id=${encodeURIComponent(process.env.DISCORD_CLIENT_ID)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=identify&prompt=none`;

if (event.queryStringParameters.state !== undefined) {
url += `&state=${encodeURIComponent(event.queryStringParameters.state)}`;
}
const state = crypto.randomBytes(25).toString("hex");

return {
statusCode: 303,
headers: {
"Location": url
"Location": `https://discord.com/api/oauth2/authorize?client_id=${encodeURIComponent(process.env.DISCORD_CLIENT_ID)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=identify&prompt=none&state=${state}`,
"Set-Cookie": `__Secure-CSRFState=${state}; Domain=${DEPLOY_PRIME_URL}; Path=/.netlify/functions/oauth-callback; Secure; HttpOnly; SameSite=Strict`
}
};
}
8 changes: 2 additions & 6 deletions public/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,8 @@ <h2 id="username" class="ml-3 mb-0 align-self-center"></h2>
};

const params = new URLSearchParams(document.location.search.substring(1));
if (!params.has("state") || !params.has("token")) {
window.location.href = `/error?msg=${encodeURIComponent("Missing state or token")}`;
}

if (params.get("state") !== localStorage.getItem("state")) {
window.location.href = `/error?msg=${encodeURIComponent("Invalid state")}`;
if (!params.has("token")) {
window.location.href = `/error?msg=${encodeURIComponent("Missing token")}`;
}

const jwt = params.get("token");
Expand Down
17 changes: 0 additions & 17 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,5 @@ <h1>Ban appeal</h1>
<div class="flex-fill text-center d-flex h-100 justify-content-center align-items-center">
<a class="btn" id="login" style="font-size: 2rem;" href="/.netlify/functions/oauth">Login with Discord</a>
</div>
<script>
function generateRandomString() {
const rand = Math.floor(Math.random() * 10);
let randStr = "";

for (let i = 0; i < 20 + rand; i++) {
randStr += String.fromCharCode(33 + Math.floor(Math.random() * 94));
}

return randStr;
}

const state = generateRandomString();
localStorage.setItem("state", state);

document.getElementById("login").href += `?state=${encodeURIComponent(state)}`;
</script>
</body>
</html>

0 comments on commit 1a12e02

Please sign in to comment.