aboutsummaryrefslogtreecommitdiff
path: root/lib/oauth2.js
blob: 5ab3f9563de85794771d4e31436c7429542caaef (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
function formatQueryString(params) {
	let l = [];
	for (let k in params) {
		l.push(encodeURIComponent(k) + "=" + encodeURIComponent(params[k]));
	}
	return l.join("&");
}

export async function fetchServerMetadata(url) {
	// TODO: handle path in config.oauth2.url
	let resp;
	try {
		resp = await fetch(url + "/.well-known/oauth-authorization-server");
		if (!resp.ok) {
			throw new Error(`HTTP error: ${resp.status} ${resp.statusText}`);
		}
	} catch (err) {
		console.warn("OAuth 2.0 server doesn't support Authorization Server Metadata (retrying with OpenID Connect Discovery): ", err);
		resp = await fetch(url + "/.well-known/openid-configuration");
		if (!resp.ok) {
			throw new Error(`HTTP error: ${resp.status} ${resp.statusText}`);
		}
	}

	let data = await resp.json();
	if (!data.issuer) {
		throw new Error("Missing issuer in response");
	}
	if (!data.authorization_endpoint) {
		throw new Error("Missing authorization_endpoint in response");
	}
	if (!data.token_endpoint) {
		throw new Error("Missing authorization_endpoint in response");
	}
	if (!data.response_types_supported.includes("code")) {
		throw new Error("Server doesn't support authorization code response type");
	}
	return data;
}

export function redirectAuthorize({ serverMetadata, clientId, redirectUri, scope }) {
	// TODO: move fragment to query string in redirect_uri
	// TODO: use the state param to prevent cross-site request
	// forgery
	let params = {
		response_type: "code",
		client_id: clientId,
		redirect_uri: redirectUri,
	};
	if (scope) {
		params.scope = scope;
	}
	window.location.assign(serverMetadata.authorization_endpoint + "?" + formatQueryString(params));
}

function buildPostHeaders(clientId, clientSecret) {
	let headers = {
		"Content-Type": "application/x-www-form-urlencoded",
		"Accept": "application/json",
	};
	if (clientSecret) {
		headers["Authorization"] = "Basic " + btoa(encodeURIComponent(clientId) + ":" + encodeURIComponent(clientSecret));
	}
	return headers;
}

export async function exchangeCode({ serverMetadata, redirectUri, code, clientId, clientSecret }) {
	let data = {
		grant_type: "authorization_code",
		code,
		redirect_uri: redirectUri,
	};
	if (!clientSecret) {
		data.client_id = clientId;
	}

	let resp = await fetch(serverMetadata.token_endpoint, {
		method: "POST",
		headers: buildPostHeaders(clientId, clientSecret),
		body: formatQueryString(data),
	});

	if (!resp.ok) {
		throw new Error(`HTTP error: ${resp.status} ${resp.statusText}`);
	}
	data = await resp.json();

	if (data.error) {
		throw new Error("Authentication failed: " + (data.error_description || data.error));
	}

	return data;
}

export async function introspectToken({ serverMetadata, token, clientId, clientSecret }) {
	let resp = await fetch(serverMetadata.introspection_endpoint, {
		method: "POST",
		headers: buildPostHeaders(clientId, clientSecret),
		body: formatQueryString({ token }),
	});
	if (!resp.ok) {
		throw new Error(`HTTP error: ${resp.status} ${resp.statusText}`);
	}
	let data = await resp.json();
	if (!data.active) {
		throw new Error("Expired token");
	}
	return data;
}