Instantly share code, notes, and snippets.
CreatedOctober 9, 2025 11:49
Save mizchi/ef846db49e8d8fade4c209445c7b0503 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
| /** | |
| 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 | |
| - discriminator | |
| - guilds | |
| Auth user | |
| - Compute(Workers) -> [Worker] | |
| - Settings -> Domain & Routes -> Cloudflare Access | |
| */ | |
| importconfigfrom"./oidc.json"with{type:"json"}; | |
| import{Hono}from"hono"; | |
| import*asjosefrom"jose"; | |
| constalgorithm={ | |
| name:"RSASSA-PKCS1-v1_5", | |
| modulusLength:2048, | |
| publicExponent:newUint8Array([0x01,0x00,0x01]), | |
| hash:{name:"SHA-256"}, | |
| }; | |
| constimportAlgo={ | |
| name:"RSASSA-PKCS1-v1_5", | |
| hash:{name:"SHA-256"}, | |
| }; | |
| asyncfunctionloadOrGenerateKeyPair(KV){ | |
| letkeyPair={}; | |
| letkeyPairJson=awaitKV.get("keys",{type:"json"}); | |
| if(keyPairJson!==null){ | |
| keyPair.publicKey=awaitcrypto.subtle.importKey( | |
| "jwk", | |
| keyPairJson.publicKey, | |
| importAlgo, | |
| true, | |
| ["verify"] | |
| ); | |
| keyPair.privateKey=awaitcrypto.subtle.importKey( | |
| "jwk", | |
| keyPairJson.privateKey, | |
| importAlgo, | |
| true, | |
| ["sign"] | |
| ); | |
| returnkeyPair; | |
| }else{ | |
| keyPair=awaitcrypto.subtle.generateKey(algorithm,true,[ | |
| "sign", | |
| "verify", | |
| ]); | |
| awaitKV.put( | |
| "keys", | |
| JSON.stringify({ | |
| privateKey:awaitcrypto.subtle.exportKey("jwk",keyPair.privateKey), | |
| publicKey:awaitcrypto.subtle.exportKey("jwk",keyPair.publicKey), | |
| }) | |
| ); | |
| returnkeyPair; | |
| } | |
| } | |
| constapp=newHono(); | |
| 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")) | |
| ){ | |
| returnc.text("Bad request.",400); | |
| } | |
| constparams=newURLSearchParams({ | |
| 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(); | |
| returnc.redirect("https://discord.com/oauth2/authorize?"+params); | |
| }); | |
| app.post("/token",async(c)=>{ | |
| constbody=awaitc.req.parseBody(); | |
| constcode=body["code"]; | |
| constparams=newURLSearchParams({ | |
| client_id:config.clientId, | |
| client_secret:config.clientSecret, | |
| redirect_uri:config.redirectURL, | |
| code:code, | |
| grant_type:"authorization_code", | |
| scope:"identify email", | |
| }).toString(); | |
| constr=awaitfetch("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)returnnewResponse("Bad request.",{status:400}); | |
| constuserInfo=awaitfetch("https://discord.com/api/v10/users/@me",{ | |
| headers:{ | |
| Authorization:"Bearer "+r["access_token"], | |
| }, | |
| }).then((res)=>res.json()); | |
| if(!userInfo["verified"])returnc.text("Bad request.",400); | |
| letservers=[]; | |
| constserverResp=awaitfetch( | |
| "https://discord.com/api/v10/users/@me/guilds", | |
| { | |
| headers:{ | |
| Authorization:"Bearer "+r["access_token"], | |
| }, | |
| } | |
| ); | |
| if(serverResp.status===200){ | |
| constserverJson=awaitserverResp.json(); | |
| servers=serverJson.map((item)=>{ | |
| returnitem["id"]; | |
| }); | |
| } | |
| letroleClaims={}; | |
| if(c.env.DISCORD_TOKEN&&"serversToCheckRolesFor"inconfig){ | |
| awaitPromise.all( | |
| config.serversToCheckRolesFor.map(async(guildId)=>{ | |
| if(servers.includes(guildId)){ | |
| letmemberPromise=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? | |
| constmemberResp=awaitmemberPromise; | |
| constmemberJson=awaitmemberResp.json(); | |
| roleClaims[`roles:${guildId}`]=memberJson.roles; | |
| } | |
| }) | |
| ); | |
| } | |
| letpreferred_username=userInfo["username"]; | |
| if(userInfo["discriminator"]&&userInfo["discriminator"]!=="0"){ | |
| preferred_username+=`#${userInfo["discriminator"]}`; | |
| } | |
| letdisplayName=userInfo["global_name"]??userInfo["username"]; | |
| constidToken=awaitnewjose.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((awaitloadOrGenerateKeyPair(c.env.KV)).privateKey); | |
| returnc.json({ | |
| ...r, | |
| scope:"identify email", | |
| id_token:idToken, | |
| }); | |
| }); | |
| app.get("/jwks.json",async(c)=>{ | |
| letpublicKey=(awaitloadOrGenerateKeyPair(c.env.KV)).publicKey; | |
| returnc.json({ | |
| keys:[ | |
| { | |
| alg:"RS256", | |
| kid:"jwtRS256", | |
| ...(awaitcrypto.subtle.exportKey("jwk",publicKey)), | |
| }, | |
| ], | |
| }); | |
| }); | |
| exportdefaultapp; |
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment