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
data/app/views/layout.erb
CHANGED
data/app/views/register.erb
CHANGED
@@ -29,7 +29,7 @@
|
|
29
29
|
</style>
|
30
30
|
|
31
31
|
<p><strong>U2F key registration<%- if @name -%> for <%= @name %><%- end -%></strong></p>
|
32
|
-
<div id="procession" class="procession_init" data-
|
32
|
+
<div id="procession" class="procession_init" data-state='<%= @state %>' data-callback='<%= @callback %>' data-reg-id='<%= @reg_id %>' data-webauthn-creation='<%= @credential_creation_options.to_json %>'>
|
33
33
|
<form id="callback_form" class="hidden" method='POST'>
|
34
34
|
<input type="hidden" name="state" value="<%= @state %>">
|
35
35
|
<input type="hidden" name="data" value="">
|
@@ -58,9 +58,12 @@
|
|
58
58
|
</div>
|
59
59
|
<div class="procession_error">
|
60
60
|
<p>Error: try again from the previous page?</p>
|
61
|
+
<p class='text-muted'><small id='error_message'></small></p>
|
61
62
|
</div>
|
62
63
|
<div class="procession_timeout">
|
63
|
-
<p>
|
64
|
+
<p>Error: The operation interrupted or timed out<p>
|
65
|
+
</div>
|
66
|
+
<div class="procession_timeout procession_error">
|
64
67
|
<p><button id="retry_button">Try again</button></p>
|
65
68
|
</div>
|
66
69
|
</div>
|
data/app/views/test.erb
CHANGED
@@ -12,7 +12,7 @@
|
|
12
12
|
<input type="hidden" name="data" value="">
|
13
13
|
</form>
|
14
14
|
|
15
|
-
<button id="register_cb_button" data-url="
|
15
|
+
<button id="register_cb_button" data-url="<%= base_url %>/register?<%= URI.encode_www_form(name: @name, comment: @comment, state: @state, callback: "js:#{request.base_url}", public_key: @public_key)%>">Register (JS callback)</button>
|
16
16
|
<script>
|
17
17
|
"use strict";
|
18
18
|
document.addEventListener("DOMContentLoaded", function() {
|
data/clarion.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.authors = ["Sorah Fukumori"]
|
10
10
|
spec.email = ["sorah@cookpad.com"]
|
11
11
|
|
12
|
-
spec.summary = %q{Web-based
|
12
|
+
spec.summary = %q{Web-based WebAuthn (U2F) Helper for CLI operations (SSH login...)}
|
13
13
|
spec.homepage = "https://github.com/sorah/clarion"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
21
21
|
spec.require_paths = ["lib"]
|
22
22
|
|
23
|
-
spec.add_dependency "
|
23
|
+
spec.add_dependency "webauthn", '>= 1.1.0'
|
24
24
|
spec.add_dependency "sinatra"
|
25
25
|
spec.add_dependency "erubis"
|
26
26
|
spec.add_dependency "aws-sdk-s3"
|
data/config.ru
CHANGED
@@ -21,6 +21,7 @@ end
|
|
21
21
|
config = {
|
22
22
|
registration_allowed_url: Regexp.new(ENV.fetch('CLARION_REGISTRATION_ALLOWED_URL')),
|
23
23
|
authn_default_expires_in: ENV.fetch('CLARION_AUTHN_DEFAULT_EXPIRES_IN', 300).to_i,
|
24
|
+
app_id: ENV['CLARION_APP_ID'],
|
24
25
|
}
|
25
26
|
|
26
27
|
case ENV.fetch('CLARION_STORE', 's3')
|
data/docs/api.md
CHANGED
@@ -21,21 +21,31 @@ application/json
|
|
21
21
|
|
22
22
|
``` json
|
23
23
|
{
|
24
|
-
"name": "
|
25
|
-
"comment": "
|
24
|
+
"name": "alice",
|
25
|
+
"comment": "SSH logging in",
|
26
26
|
"keys": [
|
27
27
|
{
|
28
|
+
"name": "my security key",
|
28
29
|
"handle": "KEYHANDLE",
|
29
30
|
"public_key": "PUBLICKEY",
|
30
|
-
"counter":
|
31
|
+
"counter": 42
|
31
32
|
}
|
32
33
|
]
|
33
34
|
}
|
34
35
|
```
|
35
36
|
|
37
|
+
- `name` (optional): used for consenting user
|
38
|
+
- `comment` (optional): used for consenting user
|
39
|
+
- keys: array of _key_ objects, which is retrievable by `/register` API
|
40
|
+
- `name` (optional)
|
41
|
+
- `handle` (required)
|
42
|
+
- `public_key` (required)
|
43
|
+
- `counter` (optional)
|
44
|
+
|
45
|
+
|
36
46
|
### Response
|
37
47
|
|
38
|
-
Same with
|
48
|
+
Same with `/api/authn/:id`
|
39
49
|
|
40
50
|
## GET `/api/authn/:id` (Check authentication result)
|
41
51
|
|
@@ -48,21 +58,33 @@ Same with /api/authn/:id
|
|
48
58
|
``` json
|
49
59
|
{
|
50
60
|
"authn": {
|
51
|
-
"id": "
|
52
|
-
"
|
53
|
-
"html_url": "
|
54
|
-
"url": "
|
55
|
-
"
|
56
|
-
"
|
61
|
+
"id": "bwsyJySllmJpFeIV4VuSPg9xO9Bdky905i48K1kA02Yd8l6C7-l4GlvPA8icYPLPxG4xkp9ePUp_3Onsemc",
|
62
|
+
"status": "open",
|
63
|
+
"html_url": "https://example.org/authn/bwsyJySllmJpFeIV4VuSPg9xO9Bdky905i48K1kA02Yd8l6C7-l4GlvPA8icYPLPxG4xkp9ePUp_3Onsemc",
|
64
|
+
"url": "https://example.org/api/authn/bwsyJySllmJpFeIV4VuSPg9xO9Bdky905i48K1kA02Yd8l6C7-l4GlvPA8icYPLPxG4xkp9ePUp_3Onsemc",
|
65
|
+
"created_at": "2017-12-08T01:14:41+09:00",
|
66
|
+
"expires_at": "2017-12-08T01:16:41+09:00",
|
67
|
+
"verified_at": "2017-12-08T01:15:41+09:00",
|
57
68
|
"verified_key": {
|
69
|
+
"name": "my security key",
|
58
70
|
"handle": "KEYHANDLE",
|
59
71
|
"public_key": "PUBLICKEY",
|
60
|
-
"counter":
|
72
|
+
"counter": 43
|
61
73
|
}
|
62
74
|
}
|
63
75
|
}
|
64
76
|
```
|
65
77
|
|
78
|
+
- `id`: authn ID
|
79
|
+
- `status`: One of = `open`, `verified`, `expired`, or `cancelled`
|
80
|
+
- `html_url`: URL of authentication page for user
|
81
|
+
- `url`: API URL to retrieve the latest authn status (`/api/authn/:id`)
|
82
|
+
- `created_at`: Creation time of authn, format is ISO8601
|
83
|
+
- `expires_at`: Expiration time of authn, format is ISO8601
|
84
|
+
- (only available when `status == "verified"`)
|
85
|
+
- `verified_at` : verification time of authn, format is ISO8601.
|
86
|
+
- `verified_key` : a _key_ object, user presented.
|
87
|
+
|
66
88
|
## GET/POST `/register` (Security Key Registration page)
|
67
89
|
|
68
90
|
Navigate user to this page for key registration. Clarion redirects back to _callback_ with registered key information.
|
@@ -75,7 +97,13 @@ form encoded body on POST, or query string on GET
|
|
75
97
|
name=NAME&comment=COMMENT&state=STATE&callback=CALLBACK&public_key=PUBKEY
|
76
98
|
```
|
77
99
|
|
78
|
-
|
100
|
+
- `NAME` (optional): Used for consenting user
|
101
|
+
- `COMMENT` (optional): Used for consenting user
|
102
|
+
- `STATE` (optional): if given, the same string will be returned in a callback
|
103
|
+
- `CALLBACK` (required): Callback URL
|
104
|
+
- may start with `js:`: When `js:$ORIGIN` (where `$ORIGIN` is a page origin) is specified, `/register` page will return a result using [window.opener.postMessage()](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
|
105
|
+
- Returned message is a JavaScript object contains a property `clarion_key`. It is a JavaScript object in the same format of POST callback.
|
106
|
+
- `PUBKEY` (required): RSA public key, in a Base64 encoded DER string.
|
79
107
|
|
80
108
|
### Response
|
81
109
|
|
@@ -94,8 +122,8 @@ state=STATE&data=DATA
|
|
94
122
|
```
|
95
123
|
|
96
124
|
- `DATA` is a JSON string `{"data": "ENCRYPT_DATA_BASE64", "key": "ENCRYPTED_SHARED_KEY_BASE64"}`.
|
97
|
-
- `ENCRYPTED_SHARED_KEY_BASE64` contains base64 encoded binary, which is a encrypted JSON string using the given RSA public key to `/register`.
|
98
|
-
- it
|
125
|
+
- `ENCRYPTED_SHARED_KEY_BASE64` contains base64 encoded binary, which is a RSA encrypted JSON string using the given RSA public key to `/register`. RSA padding mode is `PKCS1_OAEP_PADDING`.
|
126
|
+
- it decrypts like as `{"iv": "IV_BASE64", "tag": "TAG_BASE64", "key": "KEY_BASE64"}`.
|
99
127
|
- `IV_BASE64` is a base64 encoded IV.
|
100
128
|
- `TAG_BASE64` is a AES-GCM auth tag.
|
101
129
|
- `KEY_BASE64` is a base64 encoded shared key used for AES-256-GCM.
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# pam-u2f: pam 2fa example using clarion
|
2
|
+
|
3
|
+
Example usage of https://github.com/sorah/clarion
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
### Preparing Keys
|
8
|
+
|
9
|
+
Place the key information in a JSON seriarized array at `/var/cache/pam-u2f/${USER}`.
|
10
|
+
|
11
|
+
``` json
|
12
|
+
[
|
13
|
+
{
|
14
|
+
"name": "NAME",
|
15
|
+
"handle": "HANDLE",
|
16
|
+
"public_key": "PUBLICKEY",
|
17
|
+
"counter": COUNTER
|
18
|
+
}
|
19
|
+
]
|
20
|
+
```
|
21
|
+
|
22
|
+
(counter is optional)
|
23
|
+
|
24
|
+
### PAM
|
25
|
+
|
26
|
+
Use with pam_exec(8).
|
27
|
+
|
28
|
+
```
|
29
|
+
# Required
|
30
|
+
auth [success=1 default=ignore] pam_exec.so quiet /path/to/pam-u2f --check
|
31
|
+
auth requisite pam_deny.so
|
32
|
+
auth [success=ignore default=die] pam_exec.so stdout quiet /path/to/pam-u2f --initiate
|
33
|
+
auth [success=ok default=bad] pam_exec.so stdout expose_authtok quiet /path/to/pam-u2f --wait
|
34
|
+
```
|
35
|
+
|
36
|
+
```
|
37
|
+
# Optional (to combine with other 2FA PAM modules)
|
38
|
+
auth [success=ignore default=2] pam_exec.so quiet /path/to/pam-u2f --check
|
39
|
+
auth [success=ignore default=1] pam_exec.so stdout quiet /path/to/pam-u2f --initiate
|
40
|
+
auth [success=ok default=ignore] pam_exec.so stdout expose_authtok quiet /path/to/pam-u2f --wait
|
41
|
+
auth ...
|
42
|
+
```
|
43
|
+
|
44
|
+
|
45
|
+
Caveats:
|
46
|
+
|
47
|
+
1. `pam_exec` doesn't call `pam_info` with commnad's STDOUT until a command exits.
|
48
|
+
2. OpenSSH doesn't flush message until pam_prompt. So it's necessary to split the execution into two.
|
49
|
+
3. `expose_authtok` enables `pam_prompt` before command execution.
|
50
|
+
|
51
|
+
|
52
|
+
`--initiate`, `--wait` exits with a failure when a user's key doesn't exist.
|
@@ -0,0 +1,203 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Use with pam_exec(8)
|
3
|
+
require 'json'
|
4
|
+
require 'socket'
|
5
|
+
require 'syslog'
|
6
|
+
require 'uri'
|
7
|
+
require 'net/http'
|
8
|
+
require 'net/https'
|
9
|
+
require 'fileutils'
|
10
|
+
require 'digest/sha2'
|
11
|
+
|
12
|
+
class ClarionClient
|
13
|
+
def initialize(endpoint)
|
14
|
+
@endpoint = endpoint
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_authn(keys, name: nil, comment: nil)
|
18
|
+
clarion_keys = keys.map{ |_| {name: _[:name], handle: _[:handle], counter: _[:counter], public_key: _[:public_key] } }
|
19
|
+
authn = post('/api/authn', name: name, comment: comment, keys: clarion_keys)
|
20
|
+
authn['authn']
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_authn(id)
|
24
|
+
get("/api/authn/#{id}")['authn']
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def get(path)
|
30
|
+
uri = URI.parse("#{@endpoint}#{path}")
|
31
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
32
|
+
|
33
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
34
|
+
http.use_ssl = uri.scheme == 'https'
|
35
|
+
http.start do
|
36
|
+
resp = http.request(req).tap(&:value)
|
37
|
+
JSON.parse(resp.body)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def post(path, payload)
|
43
|
+
uri = URI.parse("#{@endpoint}#{path}")
|
44
|
+
req = Net::HTTP::Post.new(uri.request_uri, 'Content-Type' => 'application/json')
|
45
|
+
req.body = payload.to_json
|
46
|
+
|
47
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
48
|
+
http.use_ssl = uri.scheme == 'https'
|
49
|
+
http.start do
|
50
|
+
resp = http.request(req).tap(&:value)
|
51
|
+
JSON.parse(resp.body)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
Syslog.open("pam-u2f")
|
57
|
+
$stdout.sync = true
|
58
|
+
|
59
|
+
CLARION_URL = ARGV[0]
|
60
|
+
|
61
|
+
mode = nil
|
62
|
+
mode = :check if ARGV.delete('--check')
|
63
|
+
mode = :initiate if ARGV.delete('--initiate')
|
64
|
+
mode = :wait if ARGV.delete('--wait')
|
65
|
+
unless mode and CLARION_URL
|
66
|
+
abort "Usage: #{$0} {--check|--wait|--initiate} [CLARION_URL]"
|
67
|
+
end
|
68
|
+
|
69
|
+
KEYS_DIR = '/var/cache/pam-u2f/users'
|
70
|
+
STATE_DIR = '/run/pam-u2f'
|
71
|
+
|
72
|
+
FileUtils.mkdir_p(STATE_DIR)
|
73
|
+
|
74
|
+
clarion = ClarionClient.new(CLARION_URL)
|
75
|
+
|
76
|
+
user = ENV.fetch('PAM_USER')
|
77
|
+
key_path = File.join(KEYS_DIR, user)
|
78
|
+
state_path = File.join(STATE_DIR, Digest::SHA256.hexdigest("#{user},#{ENV['PAM_RHOST']}"))
|
79
|
+
|
80
|
+
# Not using U2F, exit
|
81
|
+
unless File.exist?(key_path)
|
82
|
+
exit 1
|
83
|
+
end
|
84
|
+
|
85
|
+
class HaveToRetry < Exception; end
|
86
|
+
|
87
|
+
begin
|
88
|
+
keys = JSON.parse(File.read(key_path), symbolize_names: true)
|
89
|
+
|
90
|
+
case mode
|
91
|
+
when :check
|
92
|
+
exit 0
|
93
|
+
when :initiate
|
94
|
+
# Create clarion authn and present URL.
|
95
|
+
File.open(state_path, File::RDWR|File::CREAT, 0600) do |io|
|
96
|
+
# Reuse existing state if possible
|
97
|
+
io.flock(File::LOCK_SH)
|
98
|
+
io.rewind
|
99
|
+
json = io.read
|
100
|
+
state = begin
|
101
|
+
JSON.parse(json, symbolize_names: true)
|
102
|
+
rescue JSON::ParserError
|
103
|
+
nil
|
104
|
+
end
|
105
|
+
|
106
|
+
authn = nil
|
107
|
+
if state
|
108
|
+
if !state.is_a?(Hash) || !state[:id]
|
109
|
+
puts "PAM-U2F ERR: state file broken @ #{Socket.gethostname} #{state_path}"
|
110
|
+
Syslog.err('%s', "Clarion Authn broken for #{user} #{ENV['PAM_RHOST']} : #{state_path}")
|
111
|
+
raise "state is broken"
|
112
|
+
end
|
113
|
+
|
114
|
+
# Check authn status (recorded in state).
|
115
|
+
begin
|
116
|
+
authn = clarion.get_authn(state[:id])
|
117
|
+
|
118
|
+
# authn should be opened to reuse.
|
119
|
+
if authn['status'] == 'open'
|
120
|
+
id = authn['id']
|
121
|
+
html_url = authn['html_url']
|
122
|
+
else
|
123
|
+
authn = nil
|
124
|
+
end
|
125
|
+
rescue Net::HTTPNotFound
|
126
|
+
authn = nil
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
unless authn
|
131
|
+
io.flock(File::LOCK_EX)
|
132
|
+
# Other process may remove a state file. If so, simply retry to create it again.
|
133
|
+
raise HaveToRetry unless File.exist?(state_path)
|
134
|
+
|
135
|
+
authn = clarion.create_authn(keys, name: user, comment: "SSH login #{ENV['PAM_RHOST']}")
|
136
|
+
id = authn && authn['id']
|
137
|
+
html_url = authn && authn['html_url']
|
138
|
+
|
139
|
+
raise "failed to create authn" unless id && html_url
|
140
|
+
io.rewind
|
141
|
+
io.puts({user: user, rhost: ENV['PAM_RHOST'], id: id, html_url: html_url}.to_json)
|
142
|
+
io.flush
|
143
|
+
io.truncate(io.pos)
|
144
|
+
end
|
145
|
+
io.flock(File::LOCK_UN)
|
146
|
+
|
147
|
+
Syslog.info('%s', "Clarion Authn created for #{user} #{ENV['PAM_RHOST']} : #{id.inspect}")
|
148
|
+
puts "PAM-U2F: #{html_url}"
|
149
|
+
puts
|
150
|
+
puts "--- Send empty password ---"
|
151
|
+
end
|
152
|
+
exit 0
|
153
|
+
when :wait
|
154
|
+
unless File.exist?(state_path)
|
155
|
+
puts "PAM-U2F ERR: state not exist @ #{Socket.gethostname} #{state_path}"
|
156
|
+
Syslog.err("%s", "called without initiate, state not exists: #{user} #{ENV['PAM_RHOST']} #{state_path}")
|
157
|
+
exit 1
|
158
|
+
end
|
159
|
+
File.open(state_path, 'r', 0600) do |io|
|
160
|
+
io.flock(File::LOCK_SH)
|
161
|
+
io.rewind
|
162
|
+
state = JSON.parse(io.read, symbolize_names: true)
|
163
|
+
id = state[:id]
|
164
|
+
|
165
|
+
authn = nil
|
166
|
+
loop do
|
167
|
+
authn = clarion.get_authn(id)
|
168
|
+
if authn['status'] != 'open'
|
169
|
+
break
|
170
|
+
end
|
171
|
+
sleep 1
|
172
|
+
end
|
173
|
+
|
174
|
+
if authn['status'] == 'verified'
|
175
|
+
key = keys.find { |_| _[:handle] == authn['verified_key']['handle'] }
|
176
|
+
Syslog.info('%s', "Clarion Authn #{user} #{ENV['PAM_RHOST']} #{id.inspect} : status=#{authn['status']} verified_key=#{key[:name].inspect}")
|
177
|
+
puts "PAM-U2F verified with key #{key[:name]}"
|
178
|
+
puts
|
179
|
+
io.flock(File::LOCK_EX)
|
180
|
+
if File.exist?(state_path)
|
181
|
+
File.unlink(state_path)
|
182
|
+
end
|
183
|
+
exit 0
|
184
|
+
end
|
185
|
+
Syslog.warning('%s', "Clarion Authn #{user} #{ENV['PAM_RHOST']} #{id.inspect} : status=#{authn['status']}")
|
186
|
+
puts "PAM-U2F authn #{authn['status']} ..."
|
187
|
+
puts
|
188
|
+
File.unlink(state_path)
|
189
|
+
exit 1
|
190
|
+
end
|
191
|
+
end
|
192
|
+
rescue HaveToRetry
|
193
|
+
retry
|
194
|
+
rescue SystemExit
|
195
|
+
raise
|
196
|
+
rescue Exception => e
|
197
|
+
puts "PAM-U2F Error"
|
198
|
+
Syslog.err('%s', "Err: #{e.inspect}\t#{e.backtrace.join("\t")}")
|
199
|
+
File.unlink(state_path) if state_path && File.exist?(state_path)
|
200
|
+
raise
|
201
|
+
end
|
202
|
+
# Fail safe
|
203
|
+
exit 1
|
data/lib/clarion/app.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'erubis'
|
2
2
|
require 'sinatra/base'
|
3
|
-
require 'u2f'
|
4
3
|
require 'securerandom'
|
5
4
|
|
6
5
|
require 'clarion/registrator'
|
@@ -56,8 +55,16 @@ module Clarion
|
|
56
55
|
context[:config]
|
57
56
|
end
|
58
57
|
|
59
|
-
def
|
60
|
-
|
58
|
+
def base_url
|
59
|
+
conf.app_id || request.base_url
|
60
|
+
end
|
61
|
+
|
62
|
+
def rp_id
|
63
|
+
conf.rp_id || request.host
|
64
|
+
end
|
65
|
+
|
66
|
+
def legacy_app_id
|
67
|
+
base_url
|
61
68
|
end
|
62
69
|
|
63
70
|
def counter
|
@@ -71,8 +78,8 @@ module Clarion
|
|
71
78
|
def render_authn_json(authn)
|
72
79
|
{
|
73
80
|
authn: authn.as_json.merge(
|
74
|
-
url: "#{
|
75
|
-
html_url: "#{
|
81
|
+
url: "#{base_url}/api/authn/#{authn.id}",
|
82
|
+
html_url: "#{base_url}/authn/#{authn.id}",
|
76
83
|
)
|
77
84
|
}.to_json
|
78
85
|
end
|
@@ -97,12 +104,12 @@ module Clarion
|
|
97
104
|
halt 410, "Authn already processed"
|
98
105
|
end
|
99
106
|
|
100
|
-
authenticator = Authenticator.new(@authn,
|
101
|
-
@
|
107
|
+
authenticator = Authenticator.new(@authn, counter, store, rp_id: rp_id, legacy_app_id: legacy_app_id)
|
108
|
+
@credential_request_options = authenticator.credential_request_options
|
102
109
|
|
103
110
|
@req_id = SecureRandom.urlsafe_base64(12)
|
104
111
|
session[:reqs] ||= {}
|
105
|
-
session[:reqs][@req_id] = {challenge:
|
112
|
+
session[:reqs][@req_id] = {challenge: authenticator.challenge}
|
106
113
|
|
107
114
|
erb :authn
|
108
115
|
end
|
@@ -130,19 +137,22 @@ module Clarion
|
|
130
137
|
end
|
131
138
|
|
132
139
|
@reg_id = SecureRandom.urlsafe_base64(12)
|
133
|
-
|
134
|
-
|
140
|
+
@name = params[:name]
|
141
|
+
# TODO: Give proper user_handle
|
142
|
+
registrator = Registrator.new(counter, rp_id: rp_id, rp_name: "Clarion: #{request.host}", display_name: @name)
|
143
|
+
@credential_creation_options = registrator.credential_creation_options
|
144
|
+
|
135
145
|
session[:regis] ||= []
|
136
146
|
session[:regis] << {
|
137
147
|
id: @reg_id,
|
138
|
-
|
148
|
+
challenge: registrator.challenge,
|
149
|
+
user_handle: registrator.user_handle,
|
139
150
|
key: public_key.to_der,
|
140
151
|
}
|
141
152
|
session[:regis].shift(session[:regis].size - 4) if session[:regis].size > 4
|
142
153
|
|
143
154
|
@callback = params[:callback]
|
144
155
|
@state = params[:state]
|
145
|
-
@name = params[:name]
|
146
156
|
@comment = params[:comment]
|
147
157
|
erb :register
|
148
158
|
end
|
@@ -153,13 +163,13 @@ module Clarion
|
|
153
163
|
|
154
164
|
post '/ui/register' do
|
155
165
|
content_type :json
|
156
|
-
unless data[:reg_id] && data[:
|
166
|
+
unless data[:reg_id] && data[:attestation_object] && data[:client_data_json]
|
157
167
|
halt 400, '{"error": "Missing params"}'
|
158
168
|
end
|
159
169
|
|
160
170
|
session[:regis] ||= []
|
161
171
|
reg = session[:regis].find { |_| _[:id] == data[:reg_id] }
|
162
|
-
unless reg && reg[:
|
172
|
+
unless reg && reg[:challenge] && reg[:user_handle] && reg[:key]
|
163
173
|
halt 400, '{"error": "Invalid :reg"}'
|
164
174
|
end
|
165
175
|
|
@@ -169,8 +179,18 @@ module Clarion
|
|
169
179
|
halt 400, '{"error": "Invalid public key"}'
|
170
180
|
end
|
171
181
|
|
172
|
-
registrator = Registrator.new(
|
173
|
-
|
182
|
+
registrator = Registrator.new(counter, rp_id: rp_id, user_handle: reg[:user_handle])
|
183
|
+
begin
|
184
|
+
key = registrator.register!(
|
185
|
+
challenge: reg[:challenge],
|
186
|
+
origin: request.base_url,
|
187
|
+
attestation_object: data[:attestation_object].unpack('m*')[0],
|
188
|
+
client_data_json: data[:client_data_json].unpack('m*')[0],
|
189
|
+
)
|
190
|
+
rescue Registrator::InvalidAttestation => e
|
191
|
+
logger.warn "invalid attestation error: #{e.inspect}"
|
192
|
+
halt 400, {user_error: true, error: "Invalid attestation"}.to_json
|
193
|
+
end
|
174
194
|
key.name = data[:name]
|
175
195
|
|
176
196
|
session[:regis].reject! { |_| _[:id] == data[:reg_id] }
|
@@ -212,7 +232,7 @@ module Clarion
|
|
212
232
|
|
213
233
|
post '/ui/verify/:id' do
|
214
234
|
content_type :json
|
215
|
-
unless data[:req_id] && data[:
|
235
|
+
unless data[:req_id] && data[:authenticator_data] && data[:client_data_json] && data[:signature] && data[:credential_id]
|
216
236
|
halt 400, '{"error": "missing params"}'
|
217
237
|
end
|
218
238
|
session[:reqs] ||= {}
|
@@ -235,17 +255,24 @@ module Clarion
|
|
235
255
|
halt 410, '{"error": "authn already processed"}'
|
236
256
|
end
|
237
257
|
|
238
|
-
authenticator = Authenticator.new(@authn,
|
258
|
+
authenticator = Authenticator.new(@authn, counter, store, rp_id: rp_id, legacy_app_id: legacy_app_id)
|
239
259
|
|
240
260
|
begin
|
241
261
|
authenticator.verify!(
|
242
|
-
challenge,
|
243
|
-
|
262
|
+
challenge: challenge,
|
263
|
+
origin: request.base_url,
|
264
|
+
credential_id: data[:credential_id],
|
265
|
+
authenticator_data: data[:authenticator_data].unpack('m*')[0],
|
266
|
+
client_data_json: data[:client_data_json].unpack('m*')[0],
|
267
|
+
signature: data[:signature].unpack('m*')[0],
|
244
268
|
)
|
245
|
-
|
246
|
-
|
269
|
+
logger.info "authn verified (#{@authn.id}) with credential_id=#{data[:credential_id]}"
|
270
|
+
rescue Authenticator::InvalidAssertion => e
|
271
|
+
logger.warn "authn verify error (#{@authn.id}; credential_id=#{data[:credential_id]}): #{e.inspect}"
|
272
|
+
halt 400, {user_error: true, error: "Invalid assertion"}.to_json
|
247
273
|
rescue Authenticator::InvalidKey => e
|
248
|
-
|
274
|
+
logger.warn "authn verify error (#{@authn.id}; credential_id=#{data[:credential_id]}): #{e.inspect}"
|
275
|
+
halt 401, {user_error: true, error: "It is an unregistered key"}.to_json
|
249
276
|
end
|
250
277
|
|
251
278
|
session[:reqs].delete data[:req_id]
|
@@ -286,7 +313,7 @@ module Clarion
|
|
286
313
|
key = conf.options[:register_test_key] ||= OpenSSL::PKey::RSA.generate(2048)
|
287
314
|
@name = 'testuser'
|
288
315
|
@comment = 'test comment'
|
289
|
-
@register_url = "#{
|
316
|
+
@register_url = "#{base_url}/register"
|
290
317
|
@callback = "#{request.base_url}/test/callback"
|
291
318
|
@public_key = [key.public_key.to_der].pack('m*').gsub(/\r?\n/, '')
|
292
319
|
@state = SecureRandom.urlsafe_base64(12)
|