Handle authentication for Reddit, and provide some helper functions for cookies and tokens.
This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/575868/1812157/Reddit%20Auth%20%28%2Aredditcom%20only%29.js
// ==UserScript==
// @name Reddit Auth (*.reddit.com only)
// @namespace https://greasyfork.org/en/users/1574555-littux
// @version 1.0.1
// @description Handle authentication for Reddit, and provide some helper functions for cookies and tokens.
// @author littux
// @match https://*.reddit.com/*
// @grant GM_xmlhttpRequest
// @connect www.reddit.com
// @icon https://b.thumbs.redditmedia.com/7GVLmrH9CdZeqXceSEWkmL8_DSUKRGUfwMxnUNh8D8A.png
// @license GPL-3.0-only
// @run-at document-start
// ==/UserScript==
unsafeWindow.__littuxUserscripts__ ??= {};
const userScripts = unsafeWindow.__littuxUserscripts__;
const gmFetch = (options) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
...options,
onload: (res) => resolve(res),
onerror: (err) => reject(err)
});
});
}
/** "ping" a URL to get its cookies by pretending to fetch an image */
const pingURL = (url) => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve();
img.onerror = () => resolve();
img.src = url + (url.includes("?") ? "&" : "?") + "v=" + Date.now();
});
}
const getCookie = async (name) => {
if (typeof window.cookieStore?.get === "function") {
return (await cookieStore.get(name))?.value ?? null;
}
const cookie = "; " + document.cookie + ";";
const searchPattern = "; " + name + "=";
const patternStartIndex = cookie.indexOf(searchPattern);
if (patternStartIndex === -1) return null;
const valueStartIndex = patternStartIndex + searchPattern.length;
return decodeURIComponent(
cookie.substring(
valueStartIndex, cookie.indexOf(";", valueStartIndex)
)
);
};
const getCookiePing = async (name, pingSrc) => {
let cookieData = await getCookie(name);
if (!cookieData) {
await pingURL(pingSrc);
cookieData = await getCookie(name);
if (!cookieData) throw new Error("Failed getting cookie '" + name + "' by fetching " + pingSrc);
}
return cookieData;
}
const _getToken = async () => {
userScripts.loid ??= await getCookie("loid");
userScripts.tokenKey ??= "rAPI:accessToken~"+userScripts.loid, userScripts.expiryKey ??= "rAPI:accessTokenExpiry~"+userScripts.loid; // Seperate tokens for different users
let token = localStorage.getItem(userScripts.tokenKey);
let expiry = Number(localStorage.getItem(userScripts.expiryKey));
if (token && expiry > Date.now()) return token;
try {
const response = await gmFetch({
anonymous: false,
method: "POST",
url: "https://www.reddit.com/svc/shreddit/token",
headers: {
"Content-Type": "application/json",
"origin": "https://www.reddit.com" // 403 if this isn't included
},
data: JSON.stringify({ csrf_token: await getCookiePing("csrf_token", "https://sh.reddit.com/404") })
});
if (response.status === 200) {
const data = JSON.parse(response.responseText);
token = data.token;
expiry = data.expires;
} else {
alert(`Error "${response.status}" while attempting to fetch authentication headers (redditNativeTranslations)`);
}
} catch (e) {
console.debug("Authentication token fetch error:", e);
}
// Save to cache
localStorage.setItem(userScripts.tokenKey, token);
localStorage.setItem(userScripts.expiryKey, expiry.toString());
console.log("Saved new auth token to cache.");
return token;
};
const getToken = async () => {
if (userScripts.pendingTokenPromise) return userScripts.pendingTokenPromise;
return userScripts.pendingTokenPromise = _getToken().finally(() => {
userScripts.pendingTokenPromise = null;
});
};