rodauth 2.4.0 → 2.9.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.
@@ -1,26 +1,29 @@
1
1
  (function() {
2
+ var pack = function(v) { return btoa(String.fromCharCode.apply(null, new Uint8Array(v))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); };
3
+ var unpack = function(v) { return Uint8Array.from(atob(v.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); };
2
4
  var element = document.getElementById('webauthn-setup-form');
3
5
  var f = function(e) {
4
6
  //console.log(e);
5
7
  e.preventDefault();
6
8
  if (navigator.credentials) {
7
9
  var opts = JSON.parse(element.getAttribute("data-credential-options"));
8
- opts.challenge = Uint8Array.from(atob(opts.challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
9
- opts.user.id = Uint8Array.from(atob(opts.user.id.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
10
+ opts.challenge = unpack(opts.challenge);
11
+ opts.user.id = unpack(opts.user.id);
12
+ opts.excludeCredentials.forEach(function(cred) { cred.id = unpack(cred.id); });
10
13
  //console.log(opts);
11
14
  navigator.credentials.create({publicKey: opts}).
12
15
  then(function(cred){
13
16
  //console.log(cred);
14
17
  //window.cred = cred
15
-
16
- var rawId = btoa(String.fromCharCode.apply(null, new Uint8Array(cred.rawId))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
18
+
19
+ var rawId = pack(cred.rawId);
17
20
  document.getElementById('webauthn-setup').value = JSON.stringify({
18
21
  type: cred.type,
19
22
  id: rawId,
20
23
  rawId: rawId,
21
24
  response: {
22
- attestationObject: btoa(String.fromCharCode.apply(null, new Uint8Array(cred.response.attestationObject))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''),
23
- clientDataJSON: btoa(String.fromCharCode.apply(null, new Uint8Array(cred.response.clientDataJSON))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
25
+ attestationObject: pack(cred.response.attestationObject),
26
+ clientDataJSON: pack(cred.response.clientDataJSON)
24
27
  }
25
28
  });
26
29
  element.removeEventListener("submit", f);
@@ -66,6 +66,7 @@ module Rodauth
66
66
  define_method(meth) do |&block|
67
67
  @auth.send(:define_method, meth, &block)
68
68
  @auth.send(:private, meth) if priv
69
+ @auth.send(:alias_method, meth, meth)
69
70
  end
70
71
  end
71
72
 
@@ -74,6 +75,7 @@ module Rodauth
74
75
  define_method(meth) do |&block|
75
76
  @auth.send(:define_method, umeth, &block)
76
77
  @auth.send(:private, umeth)
78
+ @auth.send(:alias_method, umeth, umeth)
77
79
  end
78
80
  end
79
81
 
@@ -82,6 +84,7 @@ module Rodauth
82
84
  block ||= proc{v}
83
85
  @auth.send(:define_method, meth, &block)
84
86
  @auth.send(:private, meth) if priv
87
+ @auth.send(:alias_method, meth, meth)
85
88
  end
86
89
  end
87
90
  end
@@ -120,8 +123,10 @@ module Rodauth
120
123
  define_method(handle_meth) do
121
124
  request.is send(route_meth) do
122
125
  check_csrf if check_csrf?
123
- before_rodauth
124
- send(internal_handle_meth, request)
126
+ _around_rodauth do
127
+ before_rodauth
128
+ send(internal_handle_meth, request)
129
+ end
125
130
  end
126
131
  end
127
132
 
@@ -238,6 +243,7 @@ module Rodauth
238
243
  instance_variable_set(iv, send(umeth))
239
244
  end
240
245
  end
246
+ alias_method(meth, meth)
241
247
  auth_private_methods(meth)
242
248
  end
243
249
 
@@ -288,15 +294,17 @@ module Rodauth
288
294
  end
289
295
 
290
296
  def enable(*features)
291
- new_features = features - @auth.features
292
- new_features.each{|f| load_feature(f)}
293
- @auth.features.concat(new_features)
297
+ features.each do |feature|
298
+ next if @auth.features.include?(feature)
299
+ load_feature(feature)
300
+ @auth.features << feature
301
+ end
294
302
  end
295
303
 
296
304
  private
297
305
 
298
306
  def load_feature(feature_name)
299
- require "rodauth/features/#{feature_name}"
307
+ require "rodauth/features/#{feature_name}" unless FEATURES[feature_name]
300
308
  feature = FEATURES[feature_name]
301
309
  enable(*feature.dependencies)
302
310
  extend feature.configuration
@@ -102,7 +102,6 @@ module Rodauth
102
102
  :set_redirect_error_flash,
103
103
  :set_title,
104
104
  :translate,
105
- :unverified_account_message,
106
105
  :update_session
107
106
  )
108
107
 
@@ -111,7 +110,8 @@ module Rodauth
111
110
  :account_from_session,
112
111
  :field_attributes,
113
112
  :field_error_attributes,
114
- :formatted_field_error
113
+ :formatted_field_error,
114
+ :around_rodauth
115
115
  )
116
116
 
117
117
  configuration_module_eval do
@@ -260,6 +260,7 @@ module Rodauth
260
260
  @password_field_autocomplete_value || 'current-password'
261
261
  end
262
262
 
263
+ alias account_password_hash_column account_password_hash_column
263
264
  # If the account_password_hash_column is set, the password hash is verified in
264
265
  # ruby, it will not use a database function to do so, it will check the password
265
266
  # hash using bcrypt.
@@ -459,6 +460,10 @@ module Rodauth
459
460
 
460
461
  private
461
462
 
463
+ def _around_rodauth
464
+ yield
465
+ end
466
+
462
467
  def database_function_password_match?(name, hash_id, password, salt)
463
468
  db.get(Sequel.function(function_name(name), hash_id, BCrypt::Engine.hash_secret(password, salt)))
464
469
  end
@@ -14,7 +14,7 @@ module Rodauth
14
14
  button 'Change Password'
15
15
  redirect
16
16
 
17
- auth_value_method :new_password_label, 'New Password'
17
+ translatable_method :new_password_label, 'New Password'
18
18
  auth_value_method :new_password_param, 'new-password'
19
19
 
20
20
  auth_value_methods(
@@ -26,11 +26,11 @@ module Rodauth
26
26
  require_account_session
27
27
  before_confirm_password_route
28
28
 
29
- request.get do
29
+ r.get do
30
30
  confirm_password_view
31
31
  end
32
32
 
33
- request.post do
33
+ r.post do
34
34
  if password_match?(param(password_param))
35
35
  transaction do
36
36
  before_confirm_password
@@ -0,0 +1,189 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Rodauth
4
+ Feature.define(:json, :Json) do
5
+ translatable_method :json_not_accepted_error_message, 'Unsupported Accept header. Must accept "application/json" or compatible content type'
6
+ translatable_method :json_non_post_error_message, 'non-POST method used in JSON API'
7
+ auth_value_method :json_accept_regexp, /(?:(?:\*|\bapplication)\/\*|\bapplication\/(?:vnd\.api\+)?json\b)/i
8
+ auth_value_method :json_check_accept?, true
9
+ auth_value_method :json_request_content_type_regexp, /\bapplication\/(?:vnd\.api\+)?json\b/i
10
+ auth_value_method :json_response_content_type, 'application/json'
11
+ auth_value_method :json_response_custom_error_status?, true
12
+ auth_value_method :json_response_error_status, 400
13
+ auth_value_method :json_response_error_key, "error"
14
+ auth_value_method :json_response_field_error_key, "field-error"
15
+ auth_value_method :json_response_success_key, "success"
16
+ translatable_method :non_json_request_error_message, 'Only JSON format requests are allowed'
17
+
18
+ auth_value_methods(
19
+ :only_json?,
20
+ :use_json?,
21
+ )
22
+
23
+ auth_methods(
24
+ :json_request?,
25
+ )
26
+
27
+ auth_private_methods :json_response_body
28
+
29
+ def set_field_error(field, message)
30
+ return super unless use_json?
31
+ json_response[json_response_field_error_key] = [field, message]
32
+ end
33
+
34
+ def set_error_flash(message)
35
+ return super unless use_json?
36
+ json_response[json_response_error_key] = message
37
+ end
38
+
39
+ def set_redirect_error_flash(message)
40
+ return super unless use_json?
41
+ json_response[json_response_error_key] = message
42
+ end
43
+
44
+ def set_notice_flash(message)
45
+ return super unless use_json?
46
+ json_response[json_response_success_key] = message if include_success_messages?
47
+ end
48
+
49
+ def set_notice_now_flash(message)
50
+ return super unless use_json?
51
+ json_response[json_response_success_key] = message if include_success_messages?
52
+ end
53
+
54
+ def json_request?
55
+ return @json_request if defined?(@json_request)
56
+ @json_request = request.content_type =~ json_request_content_type_regexp
57
+ end
58
+
59
+ def use_json?
60
+ json_request? || only_json?
61
+ end
62
+
63
+ def view(page, title)
64
+ return super unless use_json?
65
+ return_json_response
66
+ end
67
+
68
+ private
69
+
70
+ def before_view_recovery_codes
71
+ super if defined?(super)
72
+ if use_json?
73
+ json_response[:codes] = recovery_codes
74
+ json_response[json_response_success_key] ||= "" if include_success_messages?
75
+ end
76
+ end
77
+
78
+ def before_webauthn_setup_route
79
+ super if defined?(super)
80
+ if use_json? && !param_or_nil(webauthn_setup_param)
81
+ cred = new_webauthn_credential
82
+ json_response[webauthn_setup_param] = cred.as_json
83
+ json_response[webauthn_setup_challenge_param] = cred.challenge
84
+ json_response[webauthn_setup_challenge_hmac_param] = compute_hmac(cred.challenge)
85
+ end
86
+ end
87
+
88
+ def before_webauthn_auth_route
89
+ super if defined?(super)
90
+ if use_json? && !param_or_nil(webauthn_auth_param)
91
+ cred = webauth_credential_options_for_get
92
+ json_response[webauthn_auth_param] = cred.as_json
93
+ json_response[webauthn_auth_challenge_param] = cred.challenge
94
+ json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
95
+ end
96
+ end
97
+
98
+ def before_webauthn_login_route
99
+ super if defined?(super)
100
+ if use_json? && !param_or_nil(webauthn_auth_param) && account_from_login(param(login_param))
101
+ cred = webauth_credential_options_for_get
102
+ json_response[webauthn_auth_param] = cred.as_json
103
+ json_response[webauthn_auth_challenge_param] = cred.challenge
104
+ json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
105
+ end
106
+ end
107
+
108
+ def before_webauthn_remove_route
109
+ super if defined?(super)
110
+ if use_json? && !param_or_nil(webauthn_remove_param)
111
+ json_response[webauthn_remove_param] = account_webauthn_usage
112
+ end
113
+ end
114
+
115
+ def before_otp_setup_route
116
+ super if defined?(super)
117
+ if use_json? && otp_keys_use_hmac? && !param_or_nil(otp_setup_raw_param)
118
+ _otp_tmp_key(otp_new_secret)
119
+ json_response[otp_setup_param] = otp_user_key
120
+ json_response[otp_setup_raw_param] = otp_key
121
+ end
122
+ end
123
+
124
+ def before_rodauth
125
+ if json_request?
126
+ if json_check_accept? && (accept = request.env['HTTP_ACCEPT']) && accept !~ json_accept_regexp
127
+ response.status = 406
128
+ json_response[json_response_error_key] = json_not_accepted_error_message
129
+ _return_json_response
130
+ end
131
+
132
+ unless request.post?
133
+ response.status = 405
134
+ response.headers['Allow'] = 'POST'
135
+ json_response[json_response_error_key] = json_non_post_error_message
136
+ return_json_response
137
+ end
138
+ elsif only_json?
139
+ response.status = json_response_error_status
140
+ response.write non_json_request_error_message
141
+ request.halt
142
+ end
143
+
144
+ super
145
+ end
146
+
147
+ def redirect(_)
148
+ return super unless use_json?
149
+ return_json_response
150
+ end
151
+
152
+ def return_json_response
153
+ _return_json_response
154
+ end
155
+
156
+ def _return_json_response
157
+ response.status ||= json_response_error_status if json_response[json_response_error_key]
158
+ response['Content-Type'] ||= json_response_content_type
159
+ response.write(_json_response_body(json_response))
160
+ request.halt
161
+ end
162
+
163
+ def include_success_messages?
164
+ !json_response_success_key.nil?
165
+ end
166
+
167
+ def _json_response_body(hash)
168
+ request.send(:convert_to_json, hash)
169
+ end
170
+
171
+ def json_response
172
+ @json_response ||= {}
173
+ end
174
+
175
+ def set_redirect_error_status(status)
176
+ if use_json? && json_response_custom_error_status?
177
+ response.status = status
178
+ end
179
+ end
180
+
181
+ def set_response_error_status(status)
182
+ if use_json? && !json_response_custom_error_status?
183
+ status = json_response_error_status
184
+ end
185
+
186
+ super
187
+ end
188
+ end
189
+ end
@@ -4,41 +4,29 @@ require 'jwt'
4
4
 
5
5
  module Rodauth
6
6
  Feature.define(:jwt, :Jwt) do
7
+ depends :json
8
+
7
9
  translatable_method :invalid_jwt_format_error_message, "invalid JWT format or claim in Authorization header"
8
- translatable_method :json_non_post_error_message, 'non-POST method used in JSON API'
9
- translatable_method :json_not_accepted_error_message, 'Unsupported Accept header. Must accept "application/json" or compatible content type'
10
- auth_value_method :json_accept_regexp, /(?:(?:\*|\bapplication)\/\*|\bapplication\/(?:vnd\.api\+)?json\b)/i
11
- auth_value_method :json_request_content_type_regexp, /\bapplication\/(?:vnd\.api\+)?json\b/i
12
- auth_value_method :json_response_content_type, 'application/json'
13
- auth_value_method :json_response_error_status, 400
14
- auth_value_method :json_response_custom_error_status?, true
15
- auth_value_method :json_response_error_key, "error"
16
- auth_value_method :json_response_field_error_key, "field-error"
17
- auth_value_method :json_response_success_key, "success"
18
10
  auth_value_method :jwt_algorithm, "HS256"
19
11
  auth_value_method :jwt_authorization_ignore, /\A(?:Basic|Digest) /
20
12
  auth_value_method :jwt_authorization_remove, /\ABearer:?\s+/
21
- auth_value_method :jwt_check_accept?, true
22
13
  auth_value_method :jwt_decode_opts, {}.freeze
23
14
  auth_value_method :jwt_session_key, nil
24
15
  auth_value_method :jwt_symbolize_deeply?, false
25
- translatable_method :non_json_request_error_message, 'Only JSON format requests are allowed'
26
16
 
27
17
  auth_value_methods(
28
- :only_json?,
29
18
  :jwt_secret,
30
19
  :use_jwt?
31
20
  )
32
21
 
33
22
  auth_methods(
34
- :json_request?,
35
23
  :jwt_session_hash,
36
24
  :jwt_token,
37
25
  :session_jwt,
38
26
  :set_jwt_token
39
27
  )
40
28
 
41
- auth_private_methods :json_response_body
29
+ def_deprecated_alias :json_check_accept?, :jwt_check_accept?
42
30
 
43
31
  def session
44
32
  return @session if defined?(@session)
@@ -47,11 +35,8 @@ module Rodauth
47
35
  s = {}
48
36
  if jwt_token
49
37
  unless session_data = jwt_payload
50
- json_response[json_response_error_key] = invalid_jwt_format_error_message
51
- response.status ||= json_response_error_status
52
- response['Content-Type'] ||= json_response_content_type
53
- response.write(_json_response_body(json_response))
54
- request.halt
38
+ json_response[json_response_error_key] ||= invalid_jwt_format_error_message
39
+ _return_json_response
55
40
  end
56
41
 
57
42
  if jwt_session_key
@@ -73,37 +58,10 @@ module Rodauth
73
58
 
74
59
  def clear_session
75
60
  super
76
- set_jwt if use_jwt?
77
- end
78
-
79
- def set_field_error(field, message)
80
- return super unless use_jwt?
81
- json_response[json_response_field_error_key] = [field, message]
82
- end
83
-
84
- def set_error_flash(message)
85
- return super unless use_jwt?
86
- json_response[json_response_error_key] = message
87
- end
88
-
89
- def set_redirect_error_flash(message)
90
- return super unless use_jwt?
91
- json_response[json_response_error_key] = message
92
- end
93
-
94
- def set_notice_flash(message)
95
- return super unless use_jwt?
96
- json_response[json_response_success_key] = message if include_success_messages?
97
- end
98
-
99
- def set_notice_now_flash(message)
100
- return super unless use_jwt?
101
- json_response[json_response_success_key] = message if include_success_messages?
102
- end
103
-
104
- def json_request?
105
- return @json_request if defined?(@json_request)
106
- @json_request = request.content_type =~ json_request_content_type_regexp
61
+ if use_jwt?
62
+ session.clear
63
+ set_jwt
64
+ end
107
65
  end
108
66
 
109
67
  def jwt_secret
@@ -131,16 +89,15 @@ module Rodauth
131
89
  end
132
90
 
133
91
  def use_jwt?
134
- jwt_token || only_json? || json_request?
92
+ use_json?
135
93
  end
136
94
 
137
- def valid_jwt?
138
- !!(jwt_token && jwt_payload)
95
+ def use_json?
96
+ jwt_token || super
139
97
  end
140
98
 
141
- def view(page, title)
142
- return super unless use_jwt?
143
- return_json_response
99
+ def valid_jwt?
100
+ !!(jwt_token && jwt_payload)
144
101
  end
145
102
 
146
103
  private
@@ -150,99 +107,19 @@ module Rodauth
150
107
  super
151
108
  end
152
109
 
153
- def before_rodauth
154
- if json_request?
155
- if jwt_check_accept? && (accept = request.env['HTTP_ACCEPT']) && accept !~ json_accept_regexp
156
- response.status = 406
157
- json_response[json_response_error_key] = json_not_accepted_error_message
158
- response['Content-Type'] ||= json_response_content_type
159
- response.write(_json_response_body(json_response))
160
- request.halt
161
- end
162
-
163
- unless request.post?
164
- response.status = 405
165
- response.headers['Allow'] = 'POST'
166
- json_response[json_response_error_key] = json_non_post_error_message
167
- return_json_response
168
- end
169
- elsif only_json?
170
- response.status = json_response_error_status
171
- response.write non_json_request_error_message
172
- request.halt
173
- end
174
-
175
- super
176
- end
177
-
178
- def before_view_recovery_codes
179
- super if defined?(super)
180
- if use_jwt?
181
- json_response[:codes] = recovery_codes
182
- json_response[json_response_success_key] ||= "" if include_success_messages?
183
- end
184
- end
185
-
186
- def before_webauthn_setup_route
187
- super if defined?(super)
188
- if use_jwt? && !param_or_nil(webauthn_setup_param)
189
- cred = new_webauthn_credential
190
- json_response[webauthn_setup_param] = cred.as_json
191
- json_response[webauthn_setup_challenge_param] = cred.challenge
192
- json_response[webauthn_setup_challenge_hmac_param] = compute_hmac(cred.challenge)
193
- end
194
- end
195
-
196
- def before_webauthn_auth_route
197
- super if defined?(super)
198
- if use_jwt? && !param_or_nil(webauthn_auth_param)
199
- cred = webauth_credential_options_for_get
200
- json_response[webauthn_auth_param] = cred.as_json
201
- json_response[webauthn_auth_challenge_param] = cred.challenge
202
- json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
203
- end
204
- end
205
-
206
- def before_webauthn_login_route
207
- super if defined?(super)
208
- if use_jwt? && !param_or_nil(webauthn_auth_param) && account_from_login(param(login_param))
209
- cred = webauth_credential_options_for_get
210
- json_response[webauthn_auth_param] = cred.as_json
211
- json_response[webauthn_auth_challenge_param] = cred.challenge
212
- json_response[webauthn_auth_challenge_hmac_param] = compute_hmac(cred.challenge)
213
- end
214
- end
215
-
216
- def before_webauthn_remove_route
217
- super if defined?(super)
218
- if use_jwt? && !param_or_nil(webauthn_remove_param)
219
- json_response[webauthn_remove_param] = account_webauthn_usage
220
- end
221
- end
222
-
223
- def before_otp_setup_route
224
- super if defined?(super)
225
- if use_jwt? && otp_keys_use_hmac? && !param_or_nil(otp_setup_raw_param)
226
- _otp_tmp_key(otp_new_secret)
227
- json_response[otp_setup_param] = otp_user_key
228
- json_response[otp_setup_raw_param] = otp_key
229
- end
110
+ def _jwt_decode_opts
111
+ jwt_decode_opts
230
112
  end
231
113
 
232
114
  def jwt_payload
233
115
  return @jwt_payload if defined?(@jwt_payload)
234
- @jwt_payload = JWT.decode(jwt_token, jwt_secret, true, jwt_decode_opts.merge(:algorithm=>jwt_algorithm))[0]
235
- rescue JWT::DecodeError
236
- @jwt_payload = false
116
+ @jwt_payload = JWT.decode(jwt_token, jwt_secret, true, _jwt_decode_opts.merge(:algorithm=>jwt_algorithm))[0]
117
+ rescue JWT::DecodeError => e
118
+ rescue_jwt_payload(e)
237
119
  end
238
120
 
239
- def redirect(_)
240
- return super unless use_jwt?
241
- return_json_response
242
- end
243
-
244
- def include_success_messages?
245
- !json_response_success_key.nil?
121
+ def rescue_jwt_payload(_)
122
+ @jwt_payload = false
246
123
  end
247
124
 
248
125
  def set_session_value(key, value)
@@ -257,38 +134,13 @@ module Rodauth
257
134
  value
258
135
  end
259
136
 
260
- def json_response
261
- @json_response ||= {}
262
- end
263
-
264
- def _json_response_body(hash)
265
- request.send(:convert_to_json, hash)
266
- end
267
-
268
137
  def return_json_response
269
- response.status ||= json_response_error_status if json_response[json_response_error_key]
270
138
  set_jwt
271
- response['Content-Type'] ||= json_response_content_type
272
- response.write(_json_response_body(json_response))
273
- request.halt
139
+ super
274
140
  end
275
141
 
276
142
  def set_jwt
277
143
  set_jwt_token(session_jwt)
278
144
  end
279
-
280
- def set_redirect_error_status(status)
281
- if use_jwt? && json_response_custom_error_status?
282
- response.status = status
283
- end
284
- end
285
-
286
- def set_response_error_status(status)
287
- if use_jwt? && !json_response_custom_error_status?
288
- status = json_response_error_status
289
- end
290
-
291
- super
292
- end
293
145
  end
294
146
  end