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 +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)
|