clarion 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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">