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.
@@ -107,7 +107,6 @@
107
107
  font-size: 12px;
108
108
  }
109
109
  </style>
110
- <script src="/u2f-api.js"></script>
111
110
  </head>
112
111
 
113
112
  <body>
@@ -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-app-id="<%= @app_id %>" data-requests='<%= @requests.to_json %>' data-state='<%= @state %>' data-callback='<%= @callback %>' data-reg-id='<%= @reg_id %>'>
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>Timed out...</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>
@@ -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="/register?<%= URI.encode_www_form(name: @name, comment: @comment, state: @state, callback: "js:#{request.base_url}", public_key: @public_key)%>">Register (JS callback)</button>
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() {
@@ -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 FIDO U2F Helper for CLI operations (SSH login...)}
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 "u2f"
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')
@@ -21,21 +21,31 @@ application/json
21
21
 
22
22
  ``` json
23
23
  {
24
- "name": "USERNAME",
25
- "comment": "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": 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 /api/authn/:id
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": "AUTHN_ID",
52
- "expires_at": "EXPIRY",
53
- "html_url": "HTML_URL",
54
- "url": "API_URL",
55
- "status": "STATUS",
56
- "verified_at": "VERIFIED_AT",
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": 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
- CALLBACK 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).
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's decrypted like as `{"iv": "IV_BASE64", "tag": "TAG_BASE64", "key": "KEY_BASE64"}`.
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
@@ -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 u2f
60
- @u2f ||= U2F::U2F.new(conf.app_id || request.base_url)
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: "#{request.base_url}/api/authn/#{authn.id}",
75
- html_url: "#{request.base_url}/authn/#{authn.id}",
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, u2f, counter, store)
101
- @app_id, @requests, @challenge = authenticator.request
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: @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
- registrator = Registrator.new(u2f, counter)
134
- @app_id, @requests = registrator.request
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
- challenges: @requests.map(&:challenge),
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[:response]
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[:challenges] && reg[:key]
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(u2f, counter)
173
- key = registrator.register!(reg[:challenges], data[:response])
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[:response]
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, u2f, counter, store)
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
- data[:response]
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
- rescue U2F::Error => e
246
- halt 400, {error: "U2F Error: #{e.message}"}.to_json
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
- halt 400, {error: "It is an unregistered key"}.to_json
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 = "#{request.base_url}/register"
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)