Skip to content
This repository was archived by the owner on Nov 6, 2020. It is now read-only.

Commit bc0caeb

Browse files
committed
feat: Add javascript example
1 parent b2ea1e1 commit bc0caeb

File tree

9 files changed

+661
-24
lines changed

9 files changed

+661
-24
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ information about your WebPush feed, for instance:
2222
```
2323

2424
You then convert that to a [JWT](https://tools.ietf.org/html/rfc7519) encoded
25-
with `alg = "ES256"`
25+
with`alg = "ES256"`. The resulting token is the `Authorization` header
26+
"Bearer ..." token, the Public Key used to sign the JWT is added to
27+
the `Crypto-Key` set as "p256ecdsa=..."

js/README.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Javascript VAPID library
2+
3+
This minimal library contains a set of function you need to generate
4+
and verify VAPID headers.
5+
6+
The index.html file contains a stand-alone generator/verifier.
7+

js/decode.js

-14
This file was deleted.

js/index.html

+267
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
<!DOCTYPE html>
2+
<head>
3+
<meta charset="utf-8">
4+
<title>VAPID verification page</title>
5+
<link href="https://developer.cdn.mozilla.net/static/build/styles/mdn.6ff34abfc698.css" rel="stylesheet" />
6+
<link href="style.css" rel="stylesheet" />
7+
</head>
8+
<body class="document">
9+
<main class="document">
10+
<div class="center">
11+
<div id="document-main">
12+
<h1>VAPID verification</h1>
13+
<div id="intro" class="section">
14+
<p>This page helps construct or validate <a href="https://datatracker.ietf.org/doc/draft-thomson-webpush-vapid/">VAPID</a> header data.</p>
15+
</div>
16+
<div id="inputs" class="section">
17+
<h2>Headers</h2>
18+
<p>The headers are sent with subscription updates. They provide the site information to associate with
19+
this feed.</p>
20+
<label for="auth">Authorization Header:</label>
21+
<textarea name="auth" placeholder="Bearer abCDef..."></textarea>
22+
<label for="crypt">Crypto-Key Header:</label>
23+
<p>The public key expressed after "p256ecdsa=" can associate this feed with the dashboard."</p>
24+
<textarea name="crypt" placeholder="p256ecdsa=abCDef.."></textarea>
25+
<div class="control">
26+
<button id="check">Check headers</button>
27+
</div>
28+
</div>
29+
<div id="result" class="section">
30+
<h2>Claims</h2>
31+
<p>Claims are the information a site uses to identify itself.
32+
<div class="row">
33+
<label for="aud" title="The full URL to your site."><b>Aud</b>ience:</label>
34+
<p>The full URL to your site.</p>
35+
<input name="aud" placeholder="https://push.example.com">
36+
</div>
37+
<div class="row">
38+
<label for="sub" ><b>Sub</b>scriber:</label>
39+
<p>The administrative email address that can be contacted if there's an issue</p>
40+
<input name="sub" placeholder="mailto:[email protected]">
41+
</div>
42+
<div class="row">
43+
<label for="exp"><b>Exp</b>iration:</label>
44+
<p>Time in seconds for this claim to live. (Max: 24 hours from now)</p>
45+
<input name="exp" placeholder="Time in seconds">
46+
</div>
47+
<div class="control">
48+
<button id="gen">Generate VAPID</button>
49+
</div>
50+
<h3>Claims JSON object:</h3>
51+
<pre class="brush: js line-numbers lanuage-js">
52+
<code id="raw_claims" >None</code>
53+
</pre>
54+
<div id="ignored" class="hidden">
55+
<div class="title">The following were ignored.
56+
<div class="items"></div>
57+
</div>
58+
</div>
59+
</div>
60+
<div id="keys" class="section">
61+
<h2>Exported Keys</h2>
62+
<b>Auto-generated keys:</b>
63+
<p>These are ASN.1 DER formatted version of the public and private keys used to generate
64+
the VAPID headers. These can be useful for languages that use DER for key import.</p>
65+
<label for="priv">Private Key:</label><textarea name="priv"></textarea>
66+
<label for="pub">Public Key:</label><textarea name="pub"></textarea>
67+
</div>
68+
<div id="err" class="hidden section"></div>
69+
</div>
70+
</div>
71+
</main>
72+
<script src="vapid.js"></script>
73+
<script>
74+
75+
let err_strs = {
76+
enus: {
77+
INVALID_EXP: "Invalid Expiration",
78+
CLAIMS_FAIL: "Claims Failed",
79+
HEADER_NOPE: "Could not generate headers",
80+
BAD_AUTH_HE: "Missing Authorization Header",
81+
BAD_CRYP_HE: "Missing Crypto-Key Header",
82+
BAD_HEADERS: "Header check failed",
83+
}
84+
}
85+
86+
function error(ex=null, msg=null, clear=false) {
87+
let er = document.getElementById("err");
88+
if (clear) {
89+
er.innerHTML = "";
90+
}
91+
if (msg) {
92+
er.innerHTML += msg + "<br>";
93+
}
94+
if (ex) {
95+
er.innerHTML += `${ex.name}: ${ex.message}</br>`;
96+
}
97+
er.classList.remove("hidden");
98+
}
99+
100+
function success(claims) {
101+
for (let n of ["aud", "sub", "exp"]) {
102+
let item = document.getElementsByName(n)[0];
103+
item.value = claims[n];
104+
item.classList.add("updated");
105+
delete (claims[n]);
106+
}
107+
let err = document.getElementById("err");
108+
err.innerHTML = "";
109+
err.classList.add("hidden");
110+
let extra = JSON.stringify(claims, " ");
111+
if (extra.length > 2) {
112+
let ignored = document.getElementsById("ignored");
113+
ignored.getElementsByClassName("items")
114+
.innerHTML = extra;
115+
ignored.classList.remove("hidden");
116+
}
117+
}
118+
119+
120+
function fetchAuth(){
121+
let auth = document.getElementsByName("auth")[0];
122+
if (!auth) {
123+
return null
124+
}
125+
if (auth.value.split('.').length != 3) {
126+
throw new Error("Malformed Header");
127+
}
128+
return auth.value;
129+
}
130+
131+
function fetchCrypt(){
132+
let crypt = document.getElementsByName("crypt")[0];
133+
if (! crypt) {
134+
return null
135+
}
136+
return crypt.value;
137+
}
138+
139+
function fetchClaims(){
140+
let claims = document.getElementById("result").getElementsByTagName("input");
141+
let reply = {};
142+
let err = false;
143+
error(null, null, true);
144+
for (item of claims) {
145+
reply[item.name] = item.value;
146+
}
147+
148+
// verify aud
149+
if (! /^https?:\/\//.test(reply['aud'])) {
150+
error(null,
151+
`Invalid Audience: Use the full URL of your site e.g. "http://example.com"`);
152+
document.getElementsByName("aud")[0].classList.add("err");
153+
err = true;
154+
} else {
155+
document.getElementsByName("aud")[0].classList.remove("err");
156+
}
157+
// verify sub
158+
if (! /^mailto:.+@.+/.test(reply['sub'])) {
159+
error(null,
160+
`Invalid Subscriber: Use the email address of your site's ` +
161+
`administrative contact as a link (e.g. "mailto:[email protected]"`);
162+
document.getElementsByName("sub")[0].classList.add("err");
163+
err = true;
164+
} else {
165+
document.getElementsByName("sub")[0].classList.remove("err");
166+
}
167+
168+
// verify exp
169+
try {
170+
let expry = parseInt(reply['exp']);
171+
let now = parseInt(Date.now() * .001);
172+
if (! expry) {
173+
document.getElementsByName("exp")[0].value = now + 86400;
174+
reply['exp'] = now + 86400;
175+
}
176+
if (expry < now) {
177+
error(null,
178+
`Invalid Expiration: Already expired.`);
179+
err = true;
180+
181+
}
182+
} catch (ex) {
183+
error(ex, err_strs.enus.INVAPID_EXP);
184+
err = true;
185+
}
186+
if (err) {
187+
return null;
188+
}
189+
return reply
190+
}
191+
192+
function gen(){
193+
// clear the headers
194+
for (h of document.getElementById("inputs").getElementsByTagName("textarea")) {
195+
h.value = "";
196+
h.classList.remove("updated");
197+
}
198+
let claims = fetchClaims();
199+
if (! claims) {
200+
return
201+
}
202+
try {
203+
let rclaims = document.getElementById("raw_claims");
204+
rclaims.innerHTML = JSON.stringify(claims, null, " ");
205+
rclaims.classList.add("updated");
206+
vapid.generate_keys().then(x => {
207+
vapid.export_private_der()
208+
.then(k => document.getElementsByName("priv")[0].value = k)
209+
.catch(er => error(er, "Private Key export failed"));
210+
vapid.export_public_der()
211+
.then(k => document.getElementsByName("pub")[0].value = k)
212+
.catch(er => error(er, "Public key export failed" ));
213+
vapid.sign(claims)
214+
.then(k => {
215+
let auth = document.getElementsByName("auth")[0]
216+
auth.value = k.authorization;
217+
auth.classList.add('updated');
218+
let crypt = document.getElementsByName("crypt")[0]
219+
crypt.value = k["crypto-key"];
220+
crypt.classList.add('updated');
221+
})
222+
.catch(err => error(err, err_strs.enus.CLAIMS_FAIL));
223+
});
224+
} catch (ex) {
225+
error(ex, err_strs.enus.HEADER_NOPE);
226+
}
227+
}
228+
229+
function check(){
230+
try {
231+
// clear claims
232+
for (let item of document
233+
.getElementById("result").getElementsByTagName("input")) {
234+
item.value = "";
235+
item.classList.remove("updated");
236+
}
237+
// clear keys
238+
for (let item of document.getElementById("keys")
239+
.getElementsByTagName("textarea")) {
240+
item.value = "";
241+
item.classList.remove("updated");
242+
}
243+
let token = fetchAuth();
244+
let public_key = fetchCrypt();
245+
if ((token == null) && (pubic_key == null)) {
246+
if (token == null){
247+
error(null, err_strs.enus.BAD_AUTH_HE);
248+
return
249+
}
250+
failure(null, err_strs.enus.BAD_CRYP_HE);
251+
return
252+
}
253+
vapid.verify(token, public_key)
254+
.then(k => success(k))
255+
.catch(err => error(err, err_strs.enus.BAD_HEADERS));
256+
} catch (e) {
257+
error(e, "Header check failed");
258+
}
259+
}
260+
261+
document.getElementById("check").addEventListener("click", check);
262+
document.getElementById("gen").addEventListener("click", gen);
263+
264+
265+
</script>
266+
</body>
267+
</html>

js/style.css

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
html {font-family:Arial;}
2+
.section {position:relative; padding: .5em;
3+
width:80%; margin:0 auto;}
4+
button{border:1px solid black; border-radius: 5pt; margin:5px; font-weight:bold}
5+
.section label {display:block;position:relative;right:0;}
6+
.section textarea {width:95%;white-space:pre-wrap;
7+
position:relative; left:3%;
8+
word-break:break-all;vertical-align:top;}
9+
.hidden {display:none;}
10+
.section label {width:13em;}
11+
#err {color:red;}
12+
.err {color:red; border:1px solid red;}
13+
#raw_claims{white-space:pre-wrap;}
14+
input {
15+
transition: background-color;
16+
border:1px solid #ccc;
17+
}
18+
@keyframes update {
19+
from{background-color: auto;}
20+
to { background-color: #ffd699;}
21+
}
22+
.updated {
23+
background-color: auto;
24+
border:1px solid #ccc;
25+
animation-duration: 1s;
26+
animation-name: update;
27+
animation-iteration-count: 2;
28+
animation-direction: alternate;
29+
}
30+
p{margin:0 3%; font-size:90%;}
31+
#result input {margin-left:3%; width:20em;}
32+
.good::after {content:" ✓"; font-weight:bold;color:green;}
33+
.bad::after {content:" !"; font-weight:bold;color:red;}

0 commit comments

Comments
 (0)