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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9dc196ee5f2c8ed0672cd8ac94a7da79df64610526ba0590ee49a6a06e8a11f
4
- data.tar.gz: c4c9253becb0aa8825b3f5c3e40826f0dba345d87f97fc5625b0b46b095ab625
3
+ metadata.gz: 1be5cccde62a2aa7db2fc08a6f55f872779c0745da3daff77a04d8ae2f28e77d
4
+ data.tar.gz: 4cbd2bd4368caf6487a79a79977eb0f82b3c19cb1c10741237e27132774eeb7b
5
5
  SHA512:
6
- metadata.gz: 599032ddf520cd979702ed6abb808c1340e7dc123d38754abdecda59c97f6508bbdcb88bcdc55a9899d7f4e41ae635fecf9e591ce1dcf55ea8e6b5d31302c3d2
7
- data.tar.gz: 02a673769db0de5f84f51806c5ad130cbc278240c620bc8f35c3719b117819b63ec32fb10d9e2266809a4af51e07607e9440a1a4821185c31c0c1b459f6bb4e8
6
+ metadata.gz: 7bf00fbb11b976a954b09bb26fe950dcfccb40eed66b6a2dda726d83ef7e27400913f31058fa68f8e8328a746be06157223ef520507aa490e109d46c5d2db234
7
+ data.tar.gz: 5a0956255f6f737367476f3294d944b5761beaf8ade31524c4b3f46278c8ac3bdc6d554c3c43630cf962ef8e1fcfdce17c00ebc9c224a1956854b3d6163e364d
@@ -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
+ ![](https://img.sorah.jp/s/ssh-u2f.gif)
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
- TBD
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/public/test.erb](./app/public/test.erb), [app/public/test_callback.erb](./app/public/test_callback.erb), [app/public/test.js](./app/public/test.erb) for implementation.
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
 
@@ -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
- let unsupportedTimer = setTimeout(handleUnsupported, 3000);
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
- let submitKey = () => {
42
- processionElem.className = 'procession_contact';
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
- let payload = JSON.stringify({
45
- reg_id: regId,
46
- response: JSON.stringify(u2fResponse),
47
- name: document.getElementById("key_name").value,
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
- let handleError = (err) => {
51
- console.log(err);
52
- processionElem.className = 'procession_error';
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
- fetch(`/ui/register`, {credentials: 'include', method: 'POST', body: payload}).then((resp) => {
56
- console.log(resp);
57
- if (!resp.ok) {
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
- return resp.json().then((json) => {
62
- console.log(json);
63
- if (json.ok) {
64
- processCallback(json);
65
- } else {
66
- processionElem.className = 'procession_error';
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
- let u2fCallback = (response) => {
77
- console.log(response);
46
+ processionElem.className = 'procession_edit';
47
+ document.getElementById("key_name").focus();
48
+ };
78
49
 
79
- if (response.errorCode == window.u2f.ErrorCodes.TIMEOUT) {
80
- processionElem.className = 'procession_timeout';
81
- return;
82
- } else if (response.errorCode) {
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
- let startRequest = () => {
92
- processionElem.className = 'procession_wait';
93
- window.u2f.register(appId, requests, [], u2fCallback);
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
- document.getElementById("retry_button").addEventListener("click", (e) => {
97
- startRequest();
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
  });
@@ -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
- let unsupportedTimer = setTimeout(handleUnsupported, 3000);
10
-
11
- window.u2f.getApiVersion((ver) => {
12
- console.log(ver);
13
- clearTimeout(unsupportedTimer);
14
-
15
- let authnId = processionElem.attributes['data-authn-id'].value;
16
- let appId = processionElem.attributes['data-app-id'].value;
17
- let reqId = processionElem.attributes['data-req-id'].value;
18
- let requests = JSON.parse(processionElem.attributes['data-requests'].value);
19
- let challenge = JSON.parse(processionElem.attributes['data-challenge'].value);
20
-
21
- let requestCancel = (e) => {
22
- if (e) e.preventDefault();
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
- let handleError = (err) => {
28
- console.log(err);
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
- } else if (response.errorCode) {
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
- processionElem.className = 'procession_contact';
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
- let payload = JSON.stringify({
68
- req_id: reqId,
69
- response: JSON.stringify(response),
70
- });
45
+ const startAssertionRequest = async function() {
46
+ processionElem.className = 'procession_wait';
71
47
 
72
- let handleError = (err) => {
73
- console.log(err);
74
- processionElem.className = 'procession_error';
75
- };
76
-
77
- fetch(`/ui/verify/${authnId}`, {credentials: 'include', method: 'POST', body: payload}).then((resp) => {
78
- console.log(resp);
79
- if (!resp.ok) {
80
- processionElem.className = 'procession_error';
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
- return resp.json().then((json) => {
84
- console.log(json);
85
- if (json.ok) {
86
- processCallback(json);
87
- } else {
88
- processionElem.className = 'procession_error';
89
- }
90
- });
91
- }).catch(handleError);
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
- let startRequest = () => {
95
- processionElem.className = 'procession_wait';
96
- window.u2f.sign(appId, challenge, requests, cb);
97
- };
98
- document.getElementById("retry_button").addEventListener("click", (e) => {
99
- startRequest();
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
  });
@@ -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-app-id="<%= @app_id %>" data-requests='<%= @requests.to_json %>' data-challenge='<%= @challenge.to_json %>' data-req-id='<%= @req_id %>'>
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>Timed out...</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">