social-network
social-network/frontend/cognito-utils.mjs
1
const sha256 = async (str) => {
2
	return await crypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
3
};
4
5
const generateNonce = () => [...crypto.getRandomValues(new Uint8Array(16))].map((v) => v.toString(16).padStart(2, "0")).join("");
6
7
const base64URLEncode = (string) => {
8
	return btoa(String.fromCharCode.apply(null, new Uint8Array(string)))
9
		.replace(/\+/g, "-")
10
		.replace(/\//g, "_")
11
		.replace(/=+$/, "");
12
};
13
14
const base64URLDecode = (string) => {
15
	return atob(string
16
		.replace(/-/g, "+")
17
		.replace(/_/g, "/") +
18
		"=".repeat((4 - string.length % 4) % 4));
19
};
20
21
export const redirectToLogin = async (cognitoLoginUrl, clientId) => {
22
	const state = generateNonce();
23
	const codeVerifier = generateNonce();
24
	sessionStorage.setItem(`codeVerifier-${state}`, codeVerifier);
25
	const codeChallenge = base64URLEncode(await sha256(codeVerifier));
26
	window.location = `${cognitoLoginUrl}/login?response_type=code&client_id=${clientId}&state=${state}&code_challenge_method=S256&code_challenge=${codeChallenge}&redirect_uri=${window.location.origin}`;
27
};
28
29
export const redirectToLogout = async (cognitoLoginUrl, clientId) => {
30
	localStorage.removeItem("tokens");
31
	window.location = `${cognitoLoginUrl}/logout?client_id=${clientId}&logout_uri=${window.location.origin}`;
32
};
33
34
export const getValidTokens = async (cognitoLoginUrl, clientId, tokens) => {
35
	const {exp} = JSON.parse(base64URLDecode(tokens.access_token.split(".")[1]));
36
37
	// >5 minutes till expiration
38
	if(new Date(exp * 1000).getTime() > new Date().getTime() + 5 * 60 * 1000) {
39
		return tokens;
40
	}else {
41
		const res = await fetch(`${cognitoLoginUrl}/oauth2/token`, {
42
			method: "POST",
43
			headers: new Headers({"content-type": "application/x-www-form-urlencoded"}),
44
			body: Object.entries({
45
				"grant_type": "refresh_token",
46
				"client_id": clientId,
47
				"redirect_uri": window.location.origin,
48
				"refresh_token": tokens.refresh_token,
49
			}).map(([k, v]) => `${k}=${v}`).join("&"),
50
		});
51
		if (!res.ok) {
52
			throw new Error(await res.json());
53
		}
54
		const newTokens = {
55
			...tokens,
56
			...(await res.json()),
57
		};
58
		localStorage.setItem("tokens", JSON.stringify(newTokens));
59
		return newTokens;
60
	}
61
};
62
63
export const processLoginFlow = (cognitoLoginUrl, clientId) => async (init) => {
64
	const searchParams = new URL(location).searchParams;
65
66
	if (searchParams.get("code") !== null) {
67
		window.history.replaceState({}, document.title, "/");
68
		const state = searchParams.get("state");
69
		const codeVerifier = sessionStorage.getItem(`codeVerifier-${state}`);
70
		sessionStorage.removeItem(`codeVerifier-${state}`);
71
		if (codeVerifier === null) {
72
			throw new Error("Unexpected code");
73
		}
74
		const res = await fetch(`${cognitoLoginUrl}/oauth2/token`, {
75
			method: "POST",
76
			headers: new Headers({"content-type": "application/x-www-form-urlencoded"}),
77
			body: Object.entries({
78
				"grant_type": "authorization_code",
79
				"client_id": clientId,
80
				"code": searchParams.get("code"),
81
				"code_verifier": codeVerifier,
82
				"redirect_uri": window.location.origin,
83
			}).map(([k, v]) => `${k}=${v}`).join("&"),
84
		});
85
		if (!res.ok) {
86
			throw new Error(await res.json());
87
		}
88
		const tokens = await res.json();
89
		localStorage.setItem("tokens", JSON.stringify(tokens));
90
91
		init(tokens);
92
	}else {
93
		if (localStorage.getItem("tokens")) {
94
			const tokens = JSON.parse(localStorage.getItem("tokens"));
95
			init(tokens);
96
		}else {
97
			init(null);
98
		}
99
	}
100
};
101