240 lines
10 KiB
JavaScript
240 lines
10 KiB
JavaScript
import * as Cookie from "./Cookies.js";
|
|
|
|
export async function GetBlueskyDID(PDS, Handle) {
|
|
let DPoP = await ClientDPoPPDS("GET", PDS + "/xrpc/com.atproto.identity.resolveHandle?handle=" + Handle);
|
|
let request = fetch(PDS + "/xrpc/com.atproto.identity.resolveHandle?handle=" + Handle, { method: "GET", headers: {"Authorization": "DPoP " + Cookie.BlueskyAccessTokenCookie, "DPoP": DPoP}});
|
|
let body = await request.then((response) => response.json());
|
|
let status = await request.then((response) => response.status);
|
|
let header = await request.then((response) => response.headers.get("dpop-nonce"));
|
|
if (status == 401) {
|
|
await FixNonceMismatch(header);
|
|
request = fetch(PDS + "/xrpc/com.atproto.identity.resolveHandle?handle=" + Handle, { method: "GET", headers: {"Authorization": "DPoP " + Cookie.BlueskyAccessTokenCookie, "DPoP": DPoP}});
|
|
body = await request.then((response) => response.json());
|
|
}
|
|
return body;
|
|
}
|
|
|
|
export async function CreatePost(PDS, DID, Text) {
|
|
let Json = {
|
|
"$type": "app.bsky.feed.post",
|
|
"text": Text,
|
|
"createdAt": new Date(Date.now()).toISOString()
|
|
}
|
|
let RequestBody = {
|
|
"repo": DID,
|
|
"collection": "app.bsky.feed.post",
|
|
"record": Json
|
|
}
|
|
console.log(DID);
|
|
console.log(RequestBody.repo);
|
|
let DPoP = await ClientDPoPPDS("POST", PDS + "/xrpc/com.atproto.repo.createRecord");
|
|
let request = fetch(PDS + "/xrpc/com.atproto.repo.createRecord", { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Cookie.BlueskyAccessTokenCookie, "DPoP": DPoP}});
|
|
let body = await request.then((response) => response.json());
|
|
let status = await request.then((response) => response.status);
|
|
let header = await request.then((response) => response.headers.get("dpop-nonce"));
|
|
if (status == 401) {
|
|
await FixNonceMismatch(header);
|
|
let request = fetch(PDS + "/xrpc/com.atproto.repo.createRecord", { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Cookie.BlueskyAccessTokenCookie, "DPoP": DPoP}});
|
|
body = await request.then((response) => response.json());
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// Added after all the components: in case of nonce mismatch...
|
|
export async function FixNonceMismatch(head) {
|
|
return Cookie.InputCookie(Cookie.BlueskyNonceName, head);
|
|
}
|
|
|
|
// Component 1/4
|
|
export async function GetPDSWellKnown() {
|
|
return await fetch("https://bsky.social/.well-known/oauth-authorization-server", {method: "GET"})
|
|
.then((response) => response.json());
|
|
}
|
|
|
|
// Component 2/4
|
|
// Many thanks to https://github.com/tonyxu-io/pkce-generator. It was the base for this code.
|
|
export async function CreatePKCECodeVerifier() {
|
|
// Generate some Numbers
|
|
let Numbers = new Uint8Array(32);
|
|
crypto.getRandomValues(Numbers);
|
|
// Generate a random string of characters.
|
|
let CodeVerifier = "";
|
|
for (let i in Numbers) {
|
|
CodeVerifier += String.fromCharCode(Numbers[i]);
|
|
}
|
|
// Put this random string into Base64URL format.
|
|
CodeVerifier = btoa(CodeVerifier).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
return CodeVerifier;
|
|
}
|
|
|
|
export async function CreatePKCECodeChallenge(CodeVerifier) {
|
|
// Generate a code challenge with the code verifier.
|
|
// This is done by first SHA256 encrypting the CodeVerifier, then putting the outputted string into Base64URL format.
|
|
let CodeChallenge = btoa(await sha256(CodeVerifier)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
return CodeChallenge;
|
|
}
|
|
|
|
// Component 3/4
|
|
export async function PARrequest(PAREndpoint, State, Challenge) {
|
|
return fetch(PAREndpoint, {method: "POST", body: new URLSearchParams({ response_type: "code", code_challenge_method: "S256", scope: "atproto transition:generic", client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json", redirect_uri: "https://fedi.crowdedgames.group/HTML/setting.html", code_challenge: Challenge, state: State, login_hint: "crowdedgames.group" }), headers: {"Content-Type": "application/x-www-form-urlencoded"}});
|
|
}
|
|
|
|
export async function AuthRequest(TokenEndpoint, code, DPoP, Verify) {
|
|
return fetch(TokenEndpoint, {method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", code: code, client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json", redirect_uri: "https://fedi.crowdedgames.group/HTML/setting.html", code_verifier: Verify}), headers: { "DPoP": DPoP, "Content-Type": "application/x-www-form-urlencoded"}})
|
|
.then((response) => response.json());
|
|
}
|
|
|
|
// Component 4/4
|
|
export async function ClientDPoPToken(POSTorGET, RequestURL) {
|
|
let PublicKey = await crypto.subtle.importKey("jwk", JSON.parse(Cookie.BlueskyPublicKeyCookie), {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]);
|
|
let PrivateKey = await crypto.subtle.importKey("jwk", JSON.parse(Cookie.BlueskyPrivateKeyCookie), {name: "ECDSA", namedCurve: "P-256"}, true, ["sign"]);
|
|
|
|
// Header
|
|
var Header = {typ: "dpop+jwt", alg: "ES256", jwk:
|
|
await crypto.subtle.exportKey("jwk", PublicKey)
|
|
.then(function(response) {
|
|
delete response["key_ops"];
|
|
delete response["ext"];
|
|
delete response["alg"];
|
|
return response})
|
|
};
|
|
// Payload
|
|
var Payload = {};
|
|
Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json";
|
|
Payload.jti = crypto.randomUUID();
|
|
Payload.htm = POSTorGET;
|
|
Payload.htu = RequestURL;
|
|
Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000);
|
|
Payload.nonce = Cookie.BlueskyNonceCookie;
|
|
|
|
var sHeader = JSON.stringify(Header);
|
|
var sPayload = JSON.stringify(Payload);
|
|
var JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload,
|
|
await crypto.subtle.exportKey("jwk", PrivateKey)
|
|
.then(function(response) {
|
|
delete response["key_ops"];
|
|
delete response["ext"];
|
|
delete response["alg"];
|
|
return response})
|
|
);
|
|
return JWT;
|
|
}
|
|
|
|
// So far does nothing? Don't touch :3
|
|
export async function ClientDPoPPDS(POSTorGET, RequestURL) {
|
|
let PublicKey = await crypto.subtle.importKey("jwk", JSON.parse(Cookie.BlueskyPublicKeyCookie), {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]);
|
|
let PrivateKey = await crypto.subtle.importKey("jwk", JSON.parse(Cookie.BlueskyPrivateKeyCookie), {name: "ECDSA", namedCurve: "P-256"}, true, ["sign"]);
|
|
|
|
// Header
|
|
var Header = {typ: "dpop+jwt", alg: "ES256", jwk:
|
|
await crypto.subtle.exportKey("jwk", PublicKey)
|
|
.then(function(response) {
|
|
delete response["key_ops"];
|
|
delete response["ext"];
|
|
delete response["alg"];
|
|
return response})
|
|
};
|
|
// Payload
|
|
var Payload = {};
|
|
Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json";
|
|
Payload.jti = crypto.randomUUID();
|
|
Payload.htm = POSTorGET;
|
|
Payload.htu = RequestURL;
|
|
Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000);
|
|
Payload.nonce = Cookie.BlueskyNonceCookie;
|
|
Payload.ath = await CreatePKCECodeChallenge(Cookie.BlueskyAccessTokenCookie);
|
|
|
|
var sHeader = JSON.stringify(Header);
|
|
var sPayload = JSON.stringify(Payload);
|
|
var JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload,
|
|
await crypto.subtle.exportKey("jwk", PrivateKey)
|
|
.then(function(response) {
|
|
delete response["key_ops"];
|
|
delete response["ext"];
|
|
delete response["alg"];
|
|
return response})
|
|
);
|
|
return JWT;
|
|
}
|
|
|
|
export async function HandleAuthorization() {
|
|
// Declare Variables
|
|
let KeyPair = await crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, true, ["sign", "verify"]);
|
|
|
|
let WellKnown = await GetPDSWellKnown();
|
|
|
|
let State = crypto.randomUUID();
|
|
|
|
let PKCEverifier = await CreatePKCECodeVerifier();
|
|
let PKCEchallenge = await CreatePKCECodeChallenge(PKCEverifier);
|
|
// Save these
|
|
Cookie.InputCookie(Cookie.BlueskyPKCEVeriferName, PKCEverifier);
|
|
Cookie.InputCookie(Cookie.BlueskyPKCEChallengeName, PKCEchallenge);
|
|
// PAR request (beginning)
|
|
let PAR = PARrequest(WellKnown.pushed_authorization_request_endpoint, State, PKCEchallenge);
|
|
let body = await PAR.then((response) => response.json());
|
|
let nonce = await PAR.then((response) => response.headers.get("dpop-nonce"));
|
|
// Save nonce
|
|
Cookie.InputCookie(Cookie.BlueskyNonceName, nonce);
|
|
// Export keys
|
|
let ExportedKey1 = await crypto.subtle.exportKey("jwk", KeyPair.publicKey);
|
|
let ExportedKey2 = await crypto.subtle.exportKey("jwk", KeyPair.privateKey);
|
|
// Convert them into a good format.
|
|
Cookie.InputCookie(Cookie.BlueskyPublicKeyName, JSON.stringify(ExportedKey1));
|
|
Cookie.InputCookie(Cookie.BlueskyPrivateKeyName, JSON.stringify(ExportedKey2));
|
|
// Now we need to authenticate. Make sure the State stays the same throughout this whole process :]
|
|
document.location.href = "https://bsky.social/oauth/authorize?client_id=https://fedi.crowdedgames.group/oauth/client-metadata.json&request_uri=" + body.request_uri;
|
|
}
|
|
|
|
export async function GainTokens() {
|
|
let WellKnown = await GetPDSWellKnown();
|
|
|
|
// Check to see if something's a miss...
|
|
if ((document.location.href.split("state=").length > 1 && document.location.href.split("iss=").length > 1 && document.location.href.split("code=").length > 1) && Cookie.IsCookieReal(Cookie.BlueskyPKCEVeriferCookie) && !(Cookie.IsCookieReal(Cookie.BlueskyAccessTokenCookie))) {
|
|
// Create varaibles, be aware of waits because of internet.
|
|
let DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint);
|
|
let code = document.location.href.split("code=")[1];
|
|
// Authentication
|
|
let cookie = await Cookie.BlueskyPKCEVeriferCookie;
|
|
let Auth = await AuthRequest(WellKnown.token_endpoint, code, DPoP, cookie);
|
|
// Save the tokens and be done
|
|
Cookie.InputCookie(Cookie.BlueskyAccessTokenName, Auth.access_token);
|
|
Cookie.InputCookie(Cookie.BlueskyRefreshTokenName, Auth.refresh_token);
|
|
}
|
|
}
|
|
|
|
// Stolen from elsewhere.
|
|
// Firefox snippet; Slightly edited.
|
|
async function sha256(message) {
|
|
// encode as UTF-8
|
|
const MessageBuffer = new TextEncoder().encode(message);
|
|
|
|
// hash the message
|
|
const HashBuffer = await crypto.subtle.digest('SHA-256', MessageBuffer);
|
|
|
|
// convert ArrayBuffer to Array
|
|
const HashArray = Array.from(new Uint8Array(HashBuffer));
|
|
|
|
// convert this hashArray to a string
|
|
let string = "";
|
|
for (let i in HashArray) {
|
|
string += String.fromCharCode(HashArray[i]);
|
|
}
|
|
|
|
return string;
|
|
}
|
|
|
|
// Firefox snippet.
|
|
function ab2str(buf) {
|
|
return String.fromCharCode.apply(null, new Uint8Array(buf));
|
|
}
|
|
|
|
// Firefox snippet.
|
|
function str2ab(str) {
|
|
const buf = new ArrayBuffer(str.length);
|
|
const bufView = new Uint8Array(buf);
|
|
for (let i = 0, strLen = str.length; i < strLen; i++) {
|
|
bufView[i] = str.charCodeAt(i);
|
|
}
|
|
return buf;
|
|
}
|