Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created October 9, 2025 11:49
Show Gist options
  • Select an option

  • Save mizchi/ef846db49e8d8fade4c209445c7b0503 to your computer and use it in GitHub Desktop.

Select an option

Save mizchi/ef846db49e8d8fade4c209445c7b0503 to your computer and use it in GitHub Desktop.
/**
Discord Auth for Cloudflare Access
based on https://github.com/Erisa/discord-oidc-worker
# Create discord application
- Get client id and secret
- Set redirect URL to `https://<cloudflare-name>.cloudflareaccess.com/cdn-cgi/access/callback`
# Set oidc.json
oidc.json
{
"clientId": "<discord-client-id>",
"clientSecret": "<discord-secret>",
"redirectURL": "https://<cloudflare-name>.cloudflareaccess.com/cdn-cgi/access/callback",
"serversToCheckRolesFor": ["<discord-server-id-to-check>"]
}
# Wrangler with KV
```
$ npx wrangler kv:namespace create discord-popopo-keys
```
wrangler.json
{
"name": "discord-popopo-oidc",
"compatibility_date": "2025-10-08",
"main": "main.js",
"kv_namespaces": [
{
"binding": "KV",
"id": "<kv-namespace-id>"
}
]
}
deploy `wrangler deploy`
## Cloudflare Access Settings
- Cloudflare Access -> Application
https://one.dash.cloudflare.com/<your-cf-id>/settings/authentication
- Login method -> Add new -> OIDC
- App ID => `<discord-client-id>`
- Secret => `<discord-client-secret>`
- Token URL => `<your-deployment-url>/token`
- Auth URL => `<your-deployment-url>/authorize/guilds
- Authorization URL => `<your-deployment-url>/token`
- Certificate URL => `<your-deployment-url>/jwks.json`
- PKCE => Enabled
- OIDC Claims
- id
- email
- discriminator
- guilds
Auth user
- Compute(Workers) -> [Worker]
- Settings -> Domain & Routes -> Cloudflare Access
*/
import config from "./oidc.json" with { type: "json" };
import { Hono } from "hono";
import * as jose from "jose";
const algorithm = {
name: "RSASSA-PKCS1-v1_5",
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: "SHA-256" },
};
const importAlgo = {
name: "RSASSA-PKCS1-v1_5",
hash: { name: "SHA-256" },
};
async function loadOrGenerateKeyPair(KV) {
let keyPair = {};
let keyPairJson = await KV.get("keys", { type: "json" });
if (keyPairJson !== null) {
keyPair.publicKey = await crypto.subtle.importKey(
"jwk",
keyPairJson.publicKey,
importAlgo,
true,
["verify"]
);
keyPair.privateKey = await crypto.subtle.importKey(
"jwk",
keyPairJson.privateKey,
importAlgo,
true,
["sign"]
);
return keyPair;
} else {
keyPair = await crypto.subtle.generateKey(algorithm, true, [
"sign",
"verify",
]);
await KV.put(
"keys",
JSON.stringify({
privateKey: await crypto.subtle.exportKey("jwk", keyPair.privateKey),
publicKey: await crypto.subtle.exportKey("jwk", keyPair.publicKey),
})
);
return keyPair;
}
}
const app = new Hono();
app.get("/authorize/:scopemode", async (c) => {
if (
c.req.query("client_id") !== config.clientId ||
c.req.query("redirect_uri") !== config.redirectURL ||
!["guilds", "email"].includes(c.req.param("scopemode"))
) {
return c.text("Bad request.", 400);
}
const params = new URLSearchParams({
client_id: config.clientId,
redirect_uri: config.redirectURL,
response_type: "code",
scope:
c.req.param("scopemode") == "guilds"
? "identify email guilds"
: "identify email",
state: c.req.query("state"),
prompt: "none",
}).toString();
return c.redirect("https://discord.com/oauth2/authorize?" + params);
});
app.post("/token", async (c) => {
const body = await c.req.parseBody();
const code = body["code"];
const params = new URLSearchParams({
client_id: config.clientId,
client_secret: config.clientSecret,
redirect_uri: config.redirectURL,
code: code,
grant_type: "authorization_code",
scope: "identify email",
}).toString();
const r = await fetch("https://discord.com/api/v10/oauth2/token", {
method: "POST",
body: params,
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}).then((res) => res.json());
if (r === null) return new Response("Bad request.", { status: 400 });
const userInfo = await fetch("https://discord.com/api/v10/users/@me", {
headers: {
Authorization: "Bearer " + r["access_token"],
},
}).then((res) => res.json());
if (!userInfo["verified"]) return c.text("Bad request.", 400);
let servers = [];
const serverResp = await fetch(
"https://discord.com/api/v10/users/@me/guilds",
{
headers: {
Authorization: "Bearer " + r["access_token"],
},
}
);
if (serverResp.status === 200) {
const serverJson = await serverResp.json();
servers = serverJson.map((item) => {
return item["id"];
});
}
let roleClaims = {};
if (c.env.DISCORD_TOKEN && "serversToCheckRolesFor" in config) {
await Promise.all(
config.serversToCheckRolesFor.map(async (guildId) => {
if (servers.includes(guildId)) {
let memberPromise = fetch(
`https://discord.com/api/v10/guilds/${guildId}/members/${userInfo["id"]}`,
{
headers: {
Authorization: "Bot " + c.env.DISCORD_TOKEN,
},
}
);
// i had issues doing this any other way?
const memberResp = await memberPromise;
const memberJson = await memberResp.json();
roleClaims[`roles:${guildId}`] = memberJson.roles;
}
})
);
}
let preferred_username = userInfo["username"];
if (userInfo["discriminator"] && userInfo["discriminator"] !== "0") {
preferred_username += `#${userInfo["discriminator"]}`;
}
let displayName = userInfo["global_name"] ?? userInfo["username"];
const idToken = await new jose.SignJWT({
iss: "https://cloudflare.com",
aud: config.clientId,
preferred_username,
...userInfo,
...roleClaims,
email: userInfo["email"],
global_name: userInfo["global_name"],
name: displayName,
guilds: servers,
})
.setProtectedHeader({ alg: "RS256" })
.setExpirationTime("1h")
.setAudience(config.clientId)
.sign((await loadOrGenerateKeyPair(c.env.KV)).privateKey);
return c.json({
...r,
scope: "identify email",
id_token: idToken,
});
});
app.get("/jwks.json", async (c) => {
let publicKey = (await loadOrGenerateKeyPair(c.env.KV)).publicKey;
return c.json({
keys: [
{
alg: "RS256",
kid: "jwtRS256",
...(await crypto.subtle.exportKey("jwk", publicKey)),
},
],
});
});
export default app;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment