-
Notifications
You must be signed in to change notification settings - Fork 2
/
spa.html
311 lines (294 loc) · 13.5 KB
/
spa.html
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
<!DOCTYPE html>
<html>
<head>
<title>Single Page Demo</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<link rel="icon" href="../assets/common/images/favicon.png" referrerpolicy="no-referrer" />
<link rel="stylesheet" type="text/css" href="../assets/common/styles/ubisecure.css" referrerpolicy="no-referrer" />
<style type="text/css">
:root {
--font-size: 12pt;
--grid-main-area-width: minmax(auto, 72em);
}
textarea {
height: auto;
white-space: pre;
word-wrap: normal;
word-break: normal;
overflow: hidden;
font-family: monospace;
}
*[hidden] {
display: none;
}
</style>
<script type="module">
import { parsed } from "../assets/common/modules/document-promises.js";
import { set_button_href_handlers } from "../assets/common/modules/helper-module.js";
import { atobUrlSafe, btoaUrlSafe } from "./base64url.js";
const registration = {
issuer: "https://login.example.ubidemo.com/uas",
client_id: "SimpleSPA",
client_secret: "public",
scope: "openid api"
};
const api_endpoint = (location.hostname == "localhost") ? "http://localhost:5001/simple" : "https://ubi-simple-api.azurewebsites.net/simple";
let fetchWithToken = null;
async function ping() {
try {
const response = await fetch(`${registration.issuer}/ping`);
if (!response.ok) throw "ping invalid status";
if ("200 OK" !== await response.text()) throw "ping invalid response";
document.getElementById("ping").toggleAttribute("hidden", true);
} catch {
document.getElementById("ping").toggleAttribute("hidden", false);
document.getElementById("ping").innerText = `${registration.issuer} is not responding`;
}
}
function set_value(id, value) {
const element = document.getElementById(id);
element.value = value;
element.dispatchEvent(new CustomEvent("input"));
}
async function getConfiguration(issuer) {
const uri = `${issuer}/.well-known/openid-configuration`;
const response = await fetch(uri);
if (!response.ok) throw { error: "http_error", response: response };
return await response.json();
}
async function getJWKS(config) {
const uri = config.jwks_uri;
const response = await fetch(uri);
if (!response.ok) throw { error: "http_error", response: response };
return await response.json();
}
async function newCodeVerifier(method) {
switch (method) {
case "plain":
case "S256":
return btoaUrlSafe(Array.from(window.crypto.getRandomValues(new Uint8Array(32)), t => String.fromCharCode(t)).join(""))
case "":
case null:
return null;
default:
throw "invalid argument";
}
}
async function getCodeChallenge(method, code_verifier) {
switch (method) {
case "plain":
if (code_verifier === null) throw "invalid argument";
return code_verifier;
case "S256":
if (code_verifier === null) throw "invalid argument";
let bytes = Uint8Array.from(code_verifier, t => t.charCodeAt(0));
bytes = await window.crypto.subtle.digest("SHA-256", bytes);
return btoaUrlSafe(Array.from(new Uint8Array(bytes), t => String.fromCharCode(t)).join(""));
case "":
case null:
return null;
default:
throw "invalid argument";
}
}
async function sendAuthenticationRequest(configuration, client_id, scope) {
const authorization_request = new URL(configuration.authorization_endpoint);
authorization_request.searchParams.set("response_type", "code");
authorization_request.searchParams.set("scope", scope);
authorization_request.searchParams.set("client_id", client_id);
authorization_request.searchParams.set("redirect_uri", location.origin + location.pathname);
// nonce
const nonce = Array.from(window.crypto.getRandomValues(new Uint32Array(4)), t => t.toString(36)).join("");
authorization_request.searchParams.set("nonce", nonce);
window.localStorage.setItem("/SimpleSPA#nonce", nonce);
// code_challenge_method
const code_challenge_method = "S256";
authorization_request.searchParams.set("code_challenge_method", code_challenge_method);
// code_verifier
const code_verifier = await newCodeVerifier(code_challenge_method);
window.localStorage.setItem("/SimpleSPA#code_verifier", code_verifier);
// code_challenge
const code_challenge = await getCodeChallenge(code_challenge_method, code_verifier);
authorization_request.searchParams.set("code_challenge", code_challenge);
location.assign(authorization_request);
}
async function invokeTokenRequest(configuration, client_id, client_secret, code) {
const token_endpoint = configuration.token_endpoint;
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
const body = new URLSearchParams();
body.set("grant_type", "authorization_code");
body.set("code", code);
body.set("client_id", client_id);
body.set("client_secret", client_secret);
body.set("redirect_uri", location.origin + location.pathname);
const code_verifier = window.localStorage.getItem("/SimpleSPA#code_verifier");
if (code_verifier) {
body.set("code_verifier", code_verifier);
}
try {
const response = await fetch(token_endpoint, { method: "POST", mode: "cors", headers: headers, body: body.toString() });
if (!response.ok) throw { error: "http_error", response: response };
return await response.json();
} finally {
window.localStorage.removeItem("/SimpleSPA#code_verifier");
}
}
async function decodeJWT(jwks, jwt) {
const jws = jwt.split(".");
const header = JSON.parse(atobUrlSafe(jws[0]));
const claims = JSON.parse(atobUrlSafe(jws[1]));
const text2verify = Uint8Array.from(jws[0] + "." + jws[1], t => t.charCodeAt(0));
const signature = Uint8Array.from(atobUrlSafe(jws[2]), t => t.charCodeAt(0));
const negative = {
"header": header,
"claims": claims,
"signature": false,
"jwk": null,
};
function isSig(jwk) {
return (jwk.use == null || jwk.use == "sig");
}
function toJwk(jwk) {
return {
"kty": jwk.kty,
"n": jwk.n,
"e": jwk.e
};
}
const keys = jwks.keys
.filter(isSig)
.map(toJwk);
const RS256 = {
name: "RSASSA-PKCS1-v1_5",
hash: { name: "SHA-256" },
};
for (const jwk of keys) {
try {
const key = await window.crypto.subtle.importKey("jwk", jwk, RS256, false, ["verify"]);
const result = await window.crypto.subtle.verify(RS256, key, signature, text2verify);
if (result === true) {
return {
"header": header,
"claims": claims,
"signature": true,
"jwk": jwk,
};
}
} catch {
// ignore
}
}
return negative;
}
async function handleAuthenticationResponse() {
const params = new URLSearchParams(location.search.substr(1));
if (params.has("code")) {
window.history.replaceState(null, null, location.pathname);
const config = await getConfiguration(registration.issuer);
const tokenResponse = await invokeTokenRequest(config, registration.client_id, registration.client_secret, params.get("code"));
if ("id_token" in tokenResponse) {
const jwks = await getJWKS(config);
const id_token = await decodeJWT(jwks, tokenResponse.id_token);
const signature_status = (id_token.signature === true) ? "signature verified" : "invalid signature";
document.getElementById("signature").innerText = `(${signature_status})`;
set_value("id_token", JSON.stringify(id_token.claims, null, 2));
const nonce_status = (id_token.claims.nonce == localStorage.getItem("/SimpleSPA#nonce")) ? "nonce verified" : "invalid nonce";
document.getElementById("nonce").innerText = `(${nonce_status})`;
localStorage.removeItem("/SimpleSPA#nonce");
}
if ("access_token" in tokenResponse) {
fetchWithToken = (input, init) => {
var request = new Request(input, init);
request.headers.set("Authorization", "Bearer " + tokenResponse.access_token);
return window.fetch(request);
};
} else {
fetchWithToken = null;
}
return;
}
if (params.has("error")) {
set_value("id_token", `error=${params.get("error")}`);
}
}
async function login_click(e) {
e.preventDefault();
set_value("id_token", "");
set_value("api", "");
const config = await getConfiguration(registration.issuer);
sendAuthenticationRequest(config, registration.client_id, registration.scope);
}
async function invokeApi() {
const _fetch = fetchWithToken || window.fetch;
const response = await _fetch(api_endpoint, { mode: "cors", cache: "no-store" });
if (!response.ok) throw { error: "http_error", response: response };
return await response.json();
}
async function invoke_click(e) {
e.preventDefault();
set_value("api", "");
try {
const json = await invokeApi();
set_value("api", JSON.stringify(json, null, 2));
} catch (e) {
if (e.response.status == 401) {
set_value("api", `First click login. Details: ${e.response.headers.get("WWW-Authenticate")}`);
} else {
set_value("api", `Make sure API is running. Details: ${e.response}`);
}
}
}
async function build_page() {
await parsed;
set_button_href_handlers();
ping();
document.getElementById("login").addEventListener("click", login_click);
document.getElementById("invoke").addEventListener("click", invoke_click);
document.querySelectorAll("textarea").forEach(t => t.addEventListener("input", e => {
e.target.style.height = "auto";
e.target.style.height = e.target.scrollHeight + "px";
}));
}
build_page();
handleAuthenticationResponse();
</script>
</head>
<body>
<header>
<nav>
<button href="/" target="_self">
<icon class="home"></icon> <span>Home</span>
</button>
<button href="/SimpleSPA" target="_self">Single Page Application</button>
<button href="https://github.com/psteniusubi/SimpleSPA">Repository</button>
</nav>
<nav>
<button href="https://ubisecure.com" class="ubisecure-standard-logo-h-reverse"> </button>
</nav>
</header>
<main>
<section class="outline">
<h1>Login</h1>
<form>
<p id="ping" hidden style="color:red;"></p>
<p><button id="login" type="button">Login</button></p>
<p>ID Token <span id="signature"></span> <span id="nonce"></span></p>
<p><textarea class="flex1" id="id_token"></textarea></p>
<p>Source code of Login: <a
href="https://github.com/psteniusubi/SimpleSPA">https://github.com/psteniusubi/SimpleSPA</a></p>
</form>
</section>
<section class="outline">
<h1>API</h1>
<form>
<p><button id="invoke" type="button">Invoke API</button></p>
<p>API Response</p>
<p><textarea class="flex1" id="api" style="white-space: pre"></textarea></p>
<p>Source code of API: <a
href="https://github.com/psteniusubi/SimpleAPI">https://github.com/psteniusubi/SimpleAPI</a></p>
</form>
</section>
</main>
</body>
</html>