clarion 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitattributes +1 -0
- data/README.md +4 -2
- data/app/public/register.js +84 -79
- data/app/public/sign.js +104 -84
- data/app/views/authn.erb +19 -3
- data/app/views/layout.erb +0 -1
- data/app/views/register.erb +5 -2
- data/app/views/test.erb +1 -1
- data/clarion.gemspec +2 -2
- data/config.ru +1 -0
- data/docs/api.md +42 -14
- data/examples/pam-u2f/README.md +52 -0
- data/examples/pam-u2f/pam-u2f.rb +203 -0
- data/lib/clarion/app.rb +51 -24
- data/lib/clarion/authenticator.rb +48 -22
- data/lib/clarion/authn.rb +9 -3
- data/lib/clarion/config.rb +4 -0
- data/lib/clarion/key.rb +9 -2
- data/lib/clarion/registrator.rb +53 -9
- data/lib/clarion/version.rb +1 -1
- metadata +10 -8
- data/app/public/u2f-api.js +0 -748
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1be5cccde62a2aa7db2fc08a6f55f872779c0745da3daff77a04d8ae2f28e77d
|
4
|
+
data.tar.gz: 4cbd2bd4368caf6487a79a79977eb0f82b3c19cb1c10741237e27132774eeb7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7bf00fbb11b976a954b09bb26fe950dcfccb40eed66b6a2dda726d83ef7e27400913f31058fa68f8e8328a746be06157223ef520507aa490e109d46c5d2db234
|
7
|
+
data.tar.gz: 5a0956255f6f737367476f3294d944b5761beaf8ade31524c4b3f46278c8ac3bdc6d554c3c43630cf962ef8e1fcfdce17c00ebc9c224a1956854b3d6163e364d
|
data/.gitattributes
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
u2f-api.js linguist-generated=true
|
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Clarion: Web-based FIDO U2F helper for CLI operations (e.g. SSH Log in)
|
2
2
|
|
3
|
+

|
4
|
+
|
3
5
|
Clarion is a web-based frontend to allow remote,non-browser operations (CLI) to perform 2FA on their users.
|
4
6
|
|
5
7
|
## How it works
|
@@ -34,13 +36,13 @@ See [config.ru](./config.ru) for detailed configuration. The following environme
|
|
34
36
|
|
35
37
|
### Real world example: SSH log in
|
36
38
|
|
37
|
-
|
39
|
+
See [./examples/pam-u2f](./examples/pam-u2f)
|
38
40
|
|
39
41
|
### Test implementation
|
40
42
|
|
41
43
|
Visit `/test` exists in your application. This endpoint doesn't work for multi-process/multi-threaded deployment.
|
42
44
|
|
43
|
-
See [app/
|
45
|
+
See [app/views/test.erb](./app/views/test.erb), [app/views/test_callback.erb](./app/views/test_callback.erb), [app/public/test.js](./app/public/test.js) for implementation.
|
44
46
|
|
45
47
|
### API
|
46
48
|
|
data/app/public/register.js
CHANGED
@@ -1,104 +1,109 @@
|
|
1
1
|
"use strict";
|
2
2
|
|
3
|
-
document.addEventListener("DOMContentLoaded", function() {
|
3
|
+
document.addEventListener("DOMContentLoaded", async function() {
|
4
4
|
let processionElem = document.getElementById("procession");
|
5
5
|
|
6
6
|
let handleUnsupported = () => {
|
7
7
|
processionElem.className = 'procession_unsupported';
|
8
8
|
};
|
9
|
-
|
10
|
-
|
11
|
-
window.u2f.getApiVersion((ver) => {
|
12
|
-
console.log(ver);
|
13
|
-
clearTimeout(unsupportedTimer);
|
14
|
-
let appId = processionElem.attributes['data-app-id'].value;
|
15
|
-
let regId = processionElem.attributes['data-reg-id'].value;
|
16
|
-
let requests = JSON.parse(processionElem.attributes['data-requests'].value);
|
17
|
-
let state = processionElem.attributes['data-state'].value;
|
18
|
-
let callbackUrl = processionElem.attributes['data-callback'].value;
|
19
|
-
|
20
|
-
var u2fResponse;
|
21
|
-
|
22
|
-
let processCallback = (json) => {
|
23
|
-
processionElem.className = 'procession_ok';
|
24
|
-
|
25
|
-
if (callbackUrl.match(/^js:/)) {
|
26
|
-
if (!window.opener) {
|
27
|
-
console.log("window.opener is not truthy")
|
28
|
-
processionElem.className = 'procession_error';
|
29
|
-
return;
|
30
|
-
}
|
31
|
-
window.opener.postMessage({clarion_key: {state: state, name: json.name, data: json.encrypted_key}}, callbackUrl.slice(3));
|
32
|
-
window.close();
|
33
|
-
} else {
|
34
|
-
let form = document.getElementById("callback_form");
|
35
|
-
form.action = callbackUrl;
|
36
|
-
form.querySelector('[name=data]').value = json.encrypted_key;
|
37
|
-
form.submit();
|
38
|
-
}
|
39
|
-
}
|
9
|
+
if (!navigator.credentials) return handleUnsupported();
|
40
10
|
|
41
|
-
|
42
|
-
|
11
|
+
const regId = processionElem.attributes['data-reg-id'].value;
|
12
|
+
const state = processionElem.attributes['data-state'].value;
|
13
|
+
const callbackUrl = processionElem.attributes['data-callback'].value;
|
43
14
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
});
|
15
|
+
const creationOptions = JSON.parse(processionElem.attributes['data-webauthn-creation'].value);
|
16
|
+
creationOptions.publicKey.challenge = new Uint8Array(creationOptions.publicKey.challenge).buffer;
|
17
|
+
creationOptions.publicKey.user.id = new Uint8Array(creationOptions.publicKey.user.id).buffer;
|
18
|
+
console.log(creationOptions);
|
49
19
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
20
|
+
let attestation;
|
21
|
+
|
22
|
+
const startCreationRequest = async function() {
|
23
|
+
processionElem.className = 'procession_wait';
|
24
|
+
|
25
|
+
try {
|
26
|
+
attestation = await navigator.credentials.create(creationOptions);
|
27
|
+
console.log(attestation);
|
28
|
+
} catch (e) {
|
29
|
+
document.getElementById("error_message").innerHTML = `WebAuthn (${e.toString()})`;
|
30
|
+
processionElem.className = 'procession_error';
|
31
|
+
console.log(e);
|
54
32
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
processionElem.className = 'procession_error';
|
33
|
+
if (e instanceof DOMException) {
|
34
|
+
if (e.name == 'NotAllowedError' || e.name == 'AbortError') {
|
35
|
+
processionElem.className = 'procession_timeout';
|
59
36
|
return;
|
60
37
|
}
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
});
|
69
|
-
}).catch(handleError);
|
70
|
-
};
|
71
|
-
document.getElementById("key_name_form").addEventListener("submit", (e) => {
|
72
|
-
e.preventDefault();
|
73
|
-
if (u2fResponse) submitKey();
|
74
|
-
});
|
38
|
+
if (e.name == 'NotSupportedError') {
|
39
|
+
handleUnsupported();
|
40
|
+
return;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
return;
|
44
|
+
}
|
75
45
|
|
76
|
-
|
77
|
-
|
46
|
+
processionElem.className = 'procession_edit';
|
47
|
+
document.getElementById("key_name").focus();
|
48
|
+
};
|
78
49
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
50
|
+
const submitAttestation = async function() {
|
51
|
+
processionElem.className = 'procession_contact';
|
52
|
+
|
53
|
+
const b64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
|
54
|
+
const payload = JSON.stringify({
|
55
|
+
reg_id: regId,
|
56
|
+
name: document.getElementById("key_name").value,
|
57
|
+
attestation_object: b64(attestation.response.attestationObject),
|
58
|
+
client_data_json: b64(attestation.response.clientDataJSON),
|
59
|
+
});
|
60
|
+
try {
|
61
|
+
const resp = await fetch(`/ui/register`, {credentials: 'include', method: 'POST', body: payload});
|
62
|
+
console.log(resp);
|
63
|
+
if (!resp.ok) {
|
83
64
|
processionElem.className = 'procession_error';
|
84
65
|
return;
|
85
66
|
}
|
86
|
-
u2fResponse = response;
|
87
|
-
processionElem.className = 'procession_edit';
|
88
|
-
document.getElementById("key_name").focus();
|
89
|
-
};
|
90
67
|
|
91
|
-
|
92
|
-
|
93
|
-
|
68
|
+
const json = await resp.json();
|
69
|
+
console.log(json);
|
70
|
+
if (json.ok) {
|
71
|
+
processCallback(json);
|
72
|
+
} else {
|
73
|
+
processionElem.className = 'procession_error';
|
74
|
+
}
|
75
|
+
} catch (e) {
|
76
|
+
console.log(e);
|
77
|
+
processionElem.className = 'procession_error';
|
94
78
|
};
|
79
|
+
};
|
95
80
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
startRequest();
|
81
|
+
document.getElementById("key_name_form").addEventListener("submit", (e) => {
|
82
|
+
e.preventDefault();
|
83
|
+
if (attestation) submitAttestation();
|
101
84
|
});
|
102
85
|
|
86
|
+
const processCallback = function (json) {
|
87
|
+
processionElem.className = 'procession_ok';
|
88
|
+
|
89
|
+
if (callbackUrl.match(/^js:/)) {
|
90
|
+
if (!window.opener) {
|
91
|
+
console.log("window.opener is not truthy")
|
92
|
+
processionElem.className = 'procession_error';
|
93
|
+
return;
|
94
|
+
}
|
95
|
+
window.opener.postMessage({clarion_key: {state: state, name: json.name, data: json.encrypted_key}}, callbackUrl.slice(3));
|
96
|
+
window.close();
|
97
|
+
} else {
|
98
|
+
let form = document.getElementById("callback_form");
|
99
|
+
form.action = callbackUrl;
|
100
|
+
form.querySelector('[name=data]').value = json.encrypted_key;
|
101
|
+
form.submit();
|
102
|
+
}
|
103
|
+
};
|
103
104
|
|
105
|
+
document.getElementById("retry_button").addEventListener("click", (e) => {
|
106
|
+
startCreationRequest();
|
107
|
+
});
|
108
|
+
return startCreationRequest();
|
104
109
|
});
|
data/app/public/sign.js
CHANGED
@@ -1,105 +1,125 @@
|
|
1
1
|
"use strict";
|
2
2
|
|
3
|
-
document.addEventListener("DOMContentLoaded", function() {
|
3
|
+
document.addEventListener("DOMContentLoaded", async function() {
|
4
4
|
let processionElem = document.getElementById("procession");
|
5
5
|
|
6
6
|
let handleUnsupported = () => {
|
7
7
|
processionElem.className = 'procession_unsupported';
|
8
8
|
};
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
let payload = JSON.stringify({
|
24
|
-
req_id: reqId,
|
25
|
-
});
|
9
|
+
if (!navigator.credentials) return handleUnsupported();
|
10
|
+
|
11
|
+
const requestOptions = JSON.parse(processionElem.attributes['data-webauthn-request'].value);
|
12
|
+
requestOptions.publicKey.challenge = new Uint8Array(requestOptions.publicKey.challenge).buffer;
|
13
|
+
requestOptions.publicKey.allowCredentials = requestOptions.publicKey.allowCredentials.map((v) => ({type: v.type, id: new Uint8Array(v.id).buffer}));
|
14
|
+
console.log(requestOptions);
|
15
|
+
const authnId = processionElem.attributes['data-authn-id'].value;
|
16
|
+
const reqId = processionElem.attributes['data-req-id'].value;
|
17
|
+
|
18
|
+
const cancelRequest = async function (e) {
|
19
|
+
if (e) e.preventDefault();
|
20
|
+
const payload = JSON.stringify({
|
21
|
+
req_id: reqId,
|
22
|
+
});
|
26
23
|
|
27
|
-
|
28
|
-
|
24
|
+
try {
|
25
|
+
const resp = await fetch(`/ui/cancel/${authnId}`, {credentials: 'include', method: 'POST', body: payload});
|
26
|
+
console.log(resp);
|
27
|
+
if (!resp.ok) {
|
29
28
|
processionElem.className = 'procession_error';
|
30
|
-
};
|
31
|
-
|
32
|
-
fetch(`/ui/cancel/${authnId}`, {credentials: 'include', method: 'POST', body: payload}).then((resp) => {
|
33
|
-
console.log(resp);
|
34
|
-
if (!resp.ok) {
|
35
|
-
processionElem.className = 'procession_error';
|
36
|
-
return;
|
37
|
-
}
|
38
|
-
return resp.json().then((json) => {
|
39
|
-
console.log(json);
|
40
|
-
if (json.ok) {
|
41
|
-
processionElem.className = 'procession_cancel';
|
42
|
-
} else {
|
43
|
-
processionElem.className = 'procession_error';
|
44
|
-
}
|
45
|
-
});
|
46
|
-
}).catch(handleError);
|
47
|
-
};
|
48
|
-
document.getElementById("cancel_link").addEventListener("click", requestCancel);
|
49
|
-
|
50
|
-
let processCallback = (json) => {
|
51
|
-
processionElem.className = 'procession_ok';
|
52
|
-
if (window.opener) window.close();
|
53
|
-
}
|
54
|
-
|
55
|
-
let cb = (response) => {
|
56
|
-
console.log(response);
|
57
|
-
|
58
|
-
if (response.errorCode == window.u2f.ErrorCodes.TIMEOUT) {
|
59
|
-
processionElem.className = 'procession_timeout';
|
60
29
|
return;
|
61
|
-
}
|
30
|
+
}
|
31
|
+
const json = await resp.json();
|
32
|
+
console.log(json);
|
33
|
+
if (json.ok) {
|
34
|
+
processionElem.className = 'procession_cancel';
|
35
|
+
} else {
|
62
36
|
processionElem.className = 'procession_error';
|
63
|
-
return;
|
64
37
|
}
|
65
|
-
|
38
|
+
} catch (e) {
|
39
|
+
console.log(err);
|
40
|
+
processionElem.className = 'procession_error';
|
41
|
+
}
|
42
|
+
};
|
43
|
+
document.getElementById("cancel_link").addEventListener("click", cancelRequest);
|
66
44
|
|
67
|
-
|
68
|
-
|
69
|
-
response: JSON.stringify(response),
|
70
|
-
});
|
45
|
+
const startAssertionRequest = async function() {
|
46
|
+
processionElem.className = 'procession_wait';
|
71
47
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
48
|
+
let assertion;
|
49
|
+
try {
|
50
|
+
assertion = await navigator.credentials.get(requestOptions);
|
51
|
+
console.log(assertion);
|
52
|
+
if (!assertion) {
|
53
|
+
processionElem.className = 'procession_unambigious';
|
54
|
+
return;
|
55
|
+
}
|
56
|
+
} catch (e) {
|
57
|
+
document.getElementById("error_message").innerHTML = `WebAuthn (${e.toString()})`;
|
58
|
+
processionElem.className = 'procession_error';
|
59
|
+
console.log(e);
|
60
|
+
|
61
|
+
if (e instanceof DOMException) {
|
62
|
+
if (e.name == 'NotAllowedError') {
|
63
|
+
processionElem.className = 'procession_timeout';
|
81
64
|
return;
|
82
65
|
}
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
}
|
91
|
-
}
|
92
|
-
|
66
|
+
if (e.name == 'NotSupportedError') {
|
67
|
+
handleUnsupported();
|
68
|
+
return;
|
69
|
+
}
|
70
|
+
if (e.name == 'InvalidStateError') {
|
71
|
+
processionElem.className = 'procession_invalid';
|
72
|
+
return;
|
73
|
+
}
|
74
|
+
}
|
75
|
+
return;
|
76
|
+
}
|
93
77
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
78
|
+
processionElem.className = 'procession_contact';
|
79
|
+
|
80
|
+
const b64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
|
81
|
+
const payload = JSON.stringify({
|
82
|
+
req_id: reqId,
|
83
|
+
credential_id: assertion.id,
|
84
|
+
authenticator_data: b64(assertion.response.authenticatorData),
|
85
|
+
client_data_json: b64(assertion.response.clientDataJSON),
|
86
|
+
signature: b64(assertion.response.signature),
|
87
|
+
user_handle: b64(assertion.response.userHandle),
|
88
|
+
extension_results: assertion.getClientExtensionResults(),
|
100
89
|
});
|
101
|
-
startRequest();
|
102
|
-
});
|
103
90
|
|
91
|
+
try {
|
92
|
+
const resp = await fetch(`/ui/verify/${authnId}`, {credentials: 'include', method: 'POST', body: payload});
|
93
|
+
const json = await resp.json();
|
94
|
+
console.log(json);
|
95
|
+
if (resp.ok) {
|
96
|
+
if (json.ok) {
|
97
|
+
processionElem.className = 'procession_ok';
|
98
|
+
if (window.opener) {
|
99
|
+
window.close();
|
100
|
+
} else {
|
101
|
+
setTimeout(() => window.close(), 2000);
|
102
|
+
}
|
103
|
+
} else {
|
104
|
+
processionElem.className = 'procession_error';
|
105
|
+
}
|
106
|
+
} else {
|
107
|
+
if (resp.status == 401) {
|
108
|
+
processionElem.className = 'procession_invalid';
|
109
|
+
} else {
|
110
|
+
processionElem.className = 'procession_error';
|
111
|
+
}
|
112
|
+
}
|
113
|
+
} catch (e) {
|
114
|
+
document.getElementById("error_message").innerHTML = `Contact Error`;
|
115
|
+
console.log(e);
|
116
|
+
processionElem.className = 'procession_error';
|
117
|
+
return;
|
118
|
+
}
|
119
|
+
}
|
104
120
|
|
121
|
+
document.getElementById("retry_button").addEventListener("click", (e) => {
|
122
|
+
startAssertionRequest();
|
123
|
+
});
|
124
|
+
return startAssertionRequest();
|
105
125
|
});
|
data/app/views/authn.erb
CHANGED
@@ -23,6 +23,12 @@
|
|
23
23
|
#procession.procession_error > div.procession_error {
|
24
24
|
display: block;
|
25
25
|
}
|
26
|
+
#procession.procession_invalid > div.procession_invalid {
|
27
|
+
display: block;
|
28
|
+
}
|
29
|
+
#procession.procession_unambiguous > div.procession_invalid {
|
30
|
+
display: block;
|
31
|
+
}
|
26
32
|
#procession.procession_cancel > div.procession_cancel {
|
27
33
|
display: block;
|
28
34
|
}
|
@@ -37,7 +43,7 @@
|
|
37
43
|
<small><%= @authn.comment %></small></p>
|
38
44
|
<%- end -%>
|
39
45
|
</p>
|
40
|
-
<div id="procession" class="procession_init" data-authn-id="<%= @authn.id %>" data-
|
46
|
+
<div id="procession" class="procession_init" data-authn-id="<%= @authn.id %>" data-req-id='<%= @req_id %>' data-webauthn-request='<%= @credential_request_options.to_json %>'>
|
41
47
|
<div class="procession_init">
|
42
48
|
<p>Initializing...</p>
|
43
49
|
</div>
|
@@ -54,13 +60,23 @@
|
|
54
60
|
<p>OK: You may now close this page.</p>
|
55
61
|
</div>
|
56
62
|
<div class="procession_error">
|
57
|
-
<p>Error: Reload and try again?</p>
|
63
|
+
<p>Unexpected Error: Reload and try again?</p>
|
64
|
+
<p class='text-muted'><small id='error_message'></small></p>
|
58
65
|
</div>
|
66
|
+
<div class="procession_invalid">
|
67
|
+
<p>Error: An unregistered key presented</p>
|
68
|
+
</div>
|
69
|
+
<div class="procession_unambiguous">
|
70
|
+
<p>Error: Key Selection was unambigious<p>
|
71
|
+
</div>
|
72
|
+
|
59
73
|
<div class="procession_cancel">
|
60
74
|
<p>Cancelled: You may now close this page.</p>
|
61
75
|
</div>
|
62
76
|
<div class="procession_timeout">
|
63
|
-
<p>
|
77
|
+
<p>Error: The operation interrupted or timed out<p>
|
78
|
+
</div>
|
79
|
+
<div class="procession_timeout procession_invalid procession_error">
|
64
80
|
<p><button id="retry_button">Try again</button></p>
|
65
81
|
</div>
|
66
82
|
<div class="procession_unsupported procession_error procession_wait procession_timeout">
|