Skip to content

Instantly share code, notes, and snippets.

@ACPixel
Last active June 3, 2025 15:22
Show Gist options
  • Save ACPixel/bd71dc716126153e04e41700e8a8820e to your computer and use it in GitHub Desktop.
Save ACPixel/bd71dc716126153e04e41700e8a8820e to your computer and use it in GitHub Desktop.
const express = require("express");
const crypto = require("crypto");
const router = express.Router();
//Put your scopes here
const KICK_SCOPES = [
"user:read",
"channel:read",
"chat:write",
"events:subscribe",
];
const ENDPOINT = {
authURL: "https://id.kick.com/oauth/authorize",
tokenURL: "https://id.kick.com/oauth/token",
};
//Put your redirect url here
const REDIRECT_URL = "http://localhost:3000/oauth/kick/callback";
//Client ID in env
const KICK_CLIENT_ID = process.env.KICK_CLIENT_ID;
//Client Secret in env
const KICK_CLIENT_SECRET = process.env.KICK_CLIENT_SECRET;
// PKCE Helper Functions
//This is just a helper function to generate a random string
function generateCodeVerifier() {
const buffer = crypto.randomBytes(32);
return buffer.toString("base64url");
}
//This is just a helper function to generate a code challenge(sha256 hash of the verifier)
function generateCodeChallenge(verifier) {
const hash = crypto.createHash("sha256").update(verifier).digest();
return hash.toString("base64url");
}
// Step 1: Generate a verifier, challenge and send the user to the auth page
router.get("/oauth/kick/", (req, res) => {
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
/* IMPORTANT:
This is a VERY BAD way to store the verifier. The original non-hashed verifier is needed,
later on when swapping the code for a token.
In a real application you should either store the original verifier in your own
database, or encrypt it with a secret before including it in the state.
It's not inherently wrong to store it in the state param, but if you do,
make sure it is encrypted with a secret and not just base64 encoded.
The verifier system is used to "prove" that the request for authorization was
started by your application, and later that the code exchange was also by your application.
*/
const state = Buffer.from(JSON.stringify({ codeVerifier })).toString(
"base64",
);
//using url params to make sure that they are properly encoded
const authParams = new URLSearchParams({
client_id: KICK_CLIENT_ID,
redirect_uri: REDIRECT_URL,
response_type: "code",
scope: KICK_SCOPES.join(" "),
state,
code_challenge: codeChallenge,
code_challenge_method: "S256",
});
res.redirect(`${ENDPOINT.authURL}?${authParams.toString()}`);
});
// Step 2: Handle the redirect from the auth page
router.get("/oauth/kick/callback", async (req, res) => {
const { code, state } = req.query;
if (!code) {
return res.status(400).json({ error: "Missing authorization code" });
}
try {
//As stated above, just using base64 here for simplicity sake. Don't do this in production *please*
const { codeVerifier } = JSON.parse(
Buffer.from(state, "base64").toString(),
);
const tokenParams = new URLSearchParams({
grant_type: "authorization_code",
client_id: KICK_CLIENT_ID,
client_secret: KICK_CLIENT_SECRET,
code,
redirect_uri: REDIRECT_URL,
//This is the PKCE part. It's the ORIGINAL generated code before it was sha256 hashed.
//This is what proves to kick that you are the one that started the original auth request
code_verifier: codeVerifier,
});
const tokenResponse = await fetch(ENDPOINT.tokenURL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: tokenParams,
});
const token = await tokenResponse.json();
res.json(token);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment