170 lines
7.7 KiB
HTML
170 lines
7.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Log in with Google</title>
|
|
</head>
|
|
<body>
|
|
<h1 id="text">Log in with Google</h1>
|
|
<button id="authorize">Authorize</button>
|
|
<button id="delete">Delete every last byte of my data</button>
|
|
<p>By logging in, you agree to our <a href="/privacy">Privacy Policy</a> and <a href="/tos">Terms of Service</a></p>
|
|
<script>
|
|
// Configuration
|
|
const clientId = '165295772598-ua5imd4pduoapsh0bnu96lul2j6bhsul.apps.googleusercontent.com';
|
|
const redirectUri = 'https://sho.ailur.dev/login';
|
|
const authorizationEndpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
const tokenEndpoint = 'https://oauth2.googleapis.com/token';
|
|
const userinfoEndpoint = 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json';
|
|
|
|
// Generate a random code verifier
|
|
function generateCodeVerifier() {
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
|
|
const length = 128;
|
|
return Array.from(crypto.getRandomValues(new Uint8Array(length)))
|
|
.map((x) => charset[x % charset.length])
|
|
.join("");
|
|
}
|
|
|
|
// Create a code challenge from the code verifier using SHA-256
|
|
async function createCodeChallenge(codeVerifier) {
|
|
const buffer = new TextEncoder().encode(codeVerifier);
|
|
const hashArrayBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
|
return btoa(String.fromCharCode(...new Uint8Array(hashArrayBuffer)))
|
|
.replace(/=/g, '')
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_');
|
|
}
|
|
|
|
// Authorization function with PKCE
|
|
document.getElementById('authorize').addEventListener('click', () => {
|
|
const codeVerifier = generateCodeVerifier();
|
|
localStorage.setItem('codeVerifier', codeVerifier); // Store code verifier
|
|
createCodeChallenge(codeVerifier)
|
|
.then((codeChallenge) => {
|
|
window.location.href = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256&scope=https://www.googleapis.com/auth/youtube.readonly%20https://www.googleapis.com/auth/userinfo.profile`;
|
|
})
|
|
.catch((error) => {
|
|
console.error('Error generating code challenge:', error);
|
|
});
|
|
})
|
|
|
|
// Delete every last byte of user data
|
|
document.getElementById('delete').addEventListener('click', async () => {
|
|
document.getElementById("text").innerText = "Deleting every last byte of your data..."
|
|
let response = await fetch("/api/delete", {
|
|
method: "POST",
|
|
headers: {
|
|
"Authorization": localStorage.getItem("accessToken")
|
|
}
|
|
})
|
|
if (response.status !== 200) {
|
|
document.getElementById("text").innerText = "Theres probably an outage or something. Try again later. Can't say I didn't try."
|
|
} else {
|
|
localStorage.clear()
|
|
document.getElementById("text").innerText = "Deleted every last byte of your data, even from memory (well, when the garbage collector kicks in)."
|
|
}
|
|
})
|
|
|
|
// Parse the authorization code from the URL
|
|
function parseCodeFromUrl() {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
return urlParams.get('code');
|
|
}
|
|
|
|
// Exchange authorization code for access token
|
|
async function exchangeCodeForToken(code) {
|
|
const codeVerifier = localStorage.getItem('codeVerifier'); // Retrieve code verifier
|
|
const formData = new URLSearchParams();
|
|
formData.append('client_id', String(clientId));
|
|
formData.append('code', String(code));
|
|
formData.append('redirect_uri', String(redirectUri));
|
|
formData.append('grant_type', 'authorization_code');
|
|
formData.append('code_verifier', String(codeVerifier));
|
|
|
|
// Google, for some godforsaken reason, wants client_secret during a PKCE flow.
|
|
// What the hell? That's not remotely compatible with the PKCE RFC.
|
|
// I hope to god that Google is still doing PKCE properly even though they're asking for client_secret.
|
|
// #### you, Google, the hate is absolutely justified.
|
|
formData.append('client_secret', String('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'))
|
|
|
|
let response = await fetch(tokenEndpoint, {
|
|
method: 'POST',
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
},
|
|
body: formData
|
|
});
|
|
|
|
if (response.status !== 200) {
|
|
console.error('Failed to exchange code for token:', response.status, await response.text());
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('Successfully exchanged code for token:', data);
|
|
const accessToken = data['access_token'];
|
|
|
|
// Request userinfo with ACCESS TOKEN in bearer format
|
|
// For some other godforsaken reason, they want the access token, not the id token.
|
|
// #### you, Google.
|
|
// THAT'S NOT HOW USERINFO WORKS, GOOGLE. YOU IDIOT!
|
|
fetch(userinfoEndpoint, {
|
|
headers: {
|
|
"Authorization": "Bearer " + accessToken
|
|
}
|
|
})
|
|
.then((response) => {
|
|
async function doStuff() {
|
|
if (response.status === 200) {
|
|
const userinfoData = await response.json();
|
|
console.log("Userinfo:", userinfoData);
|
|
console.log(accessToken)
|
|
console.log("User:", userinfoData["name"]);
|
|
console.log("Sub:", userinfoData["id"]);
|
|
localStorage.removeItem("codeVerifier")
|
|
document.getElementById("text").innerText = "Authenticated, " + userinfoData.name + ", now logging into the server..."
|
|
await fetch("/api/authorize", {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
"idToken": data['id_token'],
|
|
"accessToken": accessToken,
|
|
}),
|
|
}).then(response => {
|
|
response.json().then(data => {
|
|
if (response.status === 200) {
|
|
localStorage.setItem("accessToken", data["sessionToken"])
|
|
localStorage.setItem("user", userinfoData["name"])
|
|
localStorage.setItem("oauthToken", accessToken)
|
|
localStorage.setItem("sub", userinfoData["id"])
|
|
window.location.href = "/"
|
|
} else {
|
|
document.getElementById("text").innerText = "Authentication failed"
|
|
}
|
|
})
|
|
});
|
|
} else {
|
|
document.getElementById("text").innerText = "Authentication failed"
|
|
}
|
|
}
|
|
doStuff()
|
|
});
|
|
}
|
|
|
|
// Main function to handle OAuth2 flow
|
|
async function main() {
|
|
if (localStorage.getItem("user") !== null) {
|
|
document.getElementById("text").innerText = "Welcome back, " + localStorage.getItem("user") + ". Aren't you already logged in?"
|
|
}
|
|
const code = parseCodeFromUrl();
|
|
if (code) {
|
|
await exchangeCodeForToken(code);
|
|
}
|
|
}
|
|
|
|
// Call the main function on page load
|
|
main();
|
|
</script>
|
|
</body>
|
|
</html>
|