clarion 0.3.0 → 1.0.0

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