rodauth 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eedc388675de95e1b6d8f6281bc57cefb262eec9
4
- data.tar.gz: 725710de442759eba54e3a2245e2ed08af52d479
3
+ metadata.gz: 03da9b51a0536d056d085d1d6c7d69f037e63825
4
+ data.tar.gz: 6c554cef3c2f122fce444146f7032c922ef93b9d
5
5
  SHA512:
6
- metadata.gz: c354d87afa1dac53d2589b2626a17841e33b3f3041145cf88ccaef9ad4a4041a94b1a5140b88e7fb43af7de8efde7ad26f34fec0a5f79b962617656eccb10a35
7
- data.tar.gz: acc6f4b368095dafb9d102642b5da105405475fd73fae8e31c162fe086cb8f7baf849052a9274d6e64f5988b98bf7e4319595c8a746190b1edd6dd529414ded6
6
+ metadata.gz: 37a8361ce43dfcf74bf5768e386eba59555c44fb3da0b29ae638eefa61ad6f9db70e9f25cf42831fddef200b10db9bb36fd67d60d630dfe11e96275d3f2be5a4
7
+ data.tar.gz: d88123e032493908205f0f93f93f8ae813942d3574adddb89fb77cf9053f255f3d814c9c84ae84ee5c939ac1f946c60032b6bdba89b96644abb139ffea69fd3a
data/CHANGELOG CHANGED
@@ -1,3 +1,31 @@
1
+ === 1.5.0 (2016-09-22)
2
+
3
+ * Return error instead of raising exception in the jwt feature if an invalid jwt format is submitted in the Authorization header (jeremyevans)
4
+
5
+ * Add jwt_authorization_remove configuration method to jwt feature, for regexp to remove from Authorization header before JWT processing (jeremyevans)
6
+
7
+ * Add jwt_authorization_ignore configuration method to jwt feature, for regexp to skip processing of JWTs in Authorization header (jeremyevans)
8
+
9
+ * Add json_accept_regexp configuration method to jwt feature, for the regexp used to match against the Accept header (jeremyevans)
10
+
11
+ * Add use_jwt? configuration method to jwt feature, for whether to use the JWT token or rack session for authentication information (jeremyevans)
12
+
13
+ * Add jwt_check_accept? configuration method to jwt feature, to return 406 error if Accept header is present and json is not accepted (jeremyevans)
14
+
15
+ * Add json_response_content_type configuration method to jwt feature, for the content type to set for json responses, default to application/json (jeremyevans)
16
+
17
+ * Add json_request_content_type_regexp configuration method to the jwt feature, for the regexp that recognize a request as a json request (jeremyevans)
18
+
19
+ * Add session_jwt method to the jwt feature, which returns a string for the encoded JWT for the current session (jeremyevans)
20
+
21
+ * If the only_json? setting is true, return a 400 error if the request content type to a rodauth endpoint is not json (jeremyevans)
22
+
23
+ * The only_json? setting in the jwt feature is now only true by default if :json=>:only plugin option was used (jeremyevans)
24
+
25
+ * Don't have jwt feature break if HTTP Basic/Digest authentication is used (jeremyevans)
26
+
27
+ * Add template_opts configuration method, for overriding view/method options (jeremyevans)
28
+
1
29
  === 1.4.0 (2016-08-18)
2
30
 
3
31
  * Add update_password_hash feature, for updating the password hash when the hash cost changes (jeremyevans)
@@ -897,7 +897,7 @@ authentication only for particular branches:
897
897
  # ...
898
898
  end
899
899
 
900
- == JSON API Support
900
+ === JSON API Support
901
901
 
902
902
  To add support for handling JSON responses, you can pass the +:json+
903
903
  option to the plugin, and enable the JWT feature in addition to
@@ -59,6 +59,8 @@ set_deadline_values? :: Whether deadline values should be set. True by default
59
59
  on MySQL, as that doesn't support default values that
60
60
  are not constant. Can be set to true on other databases
61
61
  if you want to vary the value based on a request parameter.
62
+ template_opts :: Any template options to pass to view/render. This can be used
63
+ to set a custom layout, for example.
62
64
  use_date_arithmetic? :: Whether the date_arithmetic extension should be loaded into
63
65
  the database. Defaults to whether deadline values should
64
66
  be set.
@@ -1,11 +1,14 @@
1
1
  = Documentation for JWT Feature
2
2
 
3
3
  The jwt feature adds support for JSON API access for all other features
4
- that ship with Rodauth, using JWT as the token format.
4
+ that ship with Rodauth, using JWT (JSON Web Tokens) to hold the
5
+ authentication data.
5
6
 
6
7
  When this feature is used, all other features become accessible via a
7
8
  JSON API. The JSON API uses the POST method for all requests, using
8
- the same parameter names as the features uses.
9
+ the same parameter names as the features uses. JSON API requests to
10
+ Rodauth endpoints that use a method other than POST will result in a
11
+ 405 Method Not Allowed response.
9
12
 
10
13
  Responses are returned as JSON hashes. In case of an error, the "error"
11
14
  entry is set to an error message, and the "field-error" entry is set to
@@ -16,20 +19,44 @@ message, though that can be disabled.
16
19
  In order to use this feature, you have to set the +jwt_secret+ configuration
17
20
  option the secret used to cryptographically protect the token.
18
21
 
22
+ To use this JSON API, when processing responses for requests to a Rodauth
23
+ endpoint, check for the Authorization header, and use the value of the
24
+ response Authorization header as the request Authorization header in
25
+ future requests, if the response Authorization header is set. If the
26
+ response Authorization header is not set, then continue to use the
27
+ previous Authorization header.
28
+
29
+ When using this feature, consider using the :json=>:only option when
30
+ loading the rodauth plugin, if you want Rodauth to only handle
31
+ JSON requests. If you don't use the :json=>:only option, the jwt feature
32
+ will probably result in an error if a request to a Rodauth endpoint comes
33
+ in with a Content-Type that isn't application/json, unless you also set
34
+ <tt>only_json? false</tt> in your rodauth configuration.
35
+
19
36
  == Auth Value Methods
20
37
 
38
+ invalid_jwt_format_error_message :: The error message to use when a JWT with an invalid format is submitted in the Authorization header.
39
+ json_accept_regexp :: The regexp to use to check the Accept header for JSON if jwt_check_accept? is true.
21
40
  json_non_post_error_message :: The error message to use when a JSON non-POST request is sent.
41
+ json_not_accepted_error_message :: The error message to display if jwt_check_accept? is true and the Accept header is present but does not match json_request_content_type_regexp.
42
+ json_request_content_type_regexp :: The regexp to use to recognize a request as a json request.
43
+ json_response_content_type :: The content type to set for json responses, application/json by default.
22
44
  json_response_error_key :: The JSON result key containing an error message, "error" by default.
23
45
  json_response_error_status :: The HTTP status code to use for JSON error responses, 400 by default.
24
46
  json_response_field_error_key :: The JSON result key containing an field error message, "field-error" by default.
25
47
  json_response_success_key :: The JSON result key containing a success message for successful request, if set. nil by default to not set success messages.
26
48
  jwt_algorithm :: The JWT algorithm to use, "HS256" by default.
27
49
  non_json_request_error_message :: The error message to use when a non-JSON request is sent and +only_json?+ is set.
28
- only_json? :: Whether to have Rodauth only allow JSON requests. True by default, which means that rodauth will issue an error for non-JSON requests.
50
+ only_json? :: Whether to have Rodauth only allow JSON requests. True by default if :json=>:only option was given when loading the plugin. If set, rodauth endpoints will issue an error for non-JSON requests.
51
+ jwt_authorization_ignore :: A regexp matched against the Authorization header, which skips JWT processing if it matches. By default, HTTP Basic and Digest authentication are ignored.
52
+ jwt_authorization_remove :: A regexp to remove from the Authorization header before processing the JWT. By default, a Bearer prefix is removed.
53
+ jwt_check_accept? :: Whether to check the Accept header to see if the client supports JSON responses, false by default for backwards compatibility.
29
54
  jwt_secret :: The JWT secret to use. Access to this should be protected the same as a session secret.
55
+ use_jwt? :: Whether to use the JWT in the Authorization header for authentication information. If false, falls back to using the rack session. By default, the Authorization header is used if it is present, if only_json? is true, or if the request uses a json content type.
30
56
 
31
57
  == Auth Methods
32
58
 
33
59
  json_request? :: Whether the current request is a JSON request, looks at the Content-Type request header by default.
34
60
  jwt_token :: Retrieve the JWT token from the request, by default taking it from the Authorization header.
61
+ session_jwt :: An encoded JWT for the current session.
35
62
  set_jwt_token(token) :: Set the JWT token in the response, by default storing it in the Authorization header.
@@ -0,0 +1,74 @@
1
+ = jwt Feature Additions/Improvements
2
+
3
+ * JSON format responses now have the response content type set to
4
+ application/json.
5
+
6
+ * The jwt feature now does not break if HTTP Basic or Digest
7
+ authentication is used.
8
+
9
+ * If jwt_check_accept? is true, Rodauth will return a 406 error if
10
+ a request Accept header is provided and it does not indicate that
11
+ JSON is acceptable.
12
+
13
+ * Many new configuration methods have been added:
14
+
15
+ * invalid_jwt_format_error_message: The error message to use when a
16
+ JWT with an invalid format is submitted in the Authorization
17
+ header.
18
+
19
+ * json_accept_regexp: The regexp to use to check the Accept header
20
+ for JSON if jwt_check_accept? is true.
21
+
22
+ * json_not_accepted_error_message: The error message to display if
23
+ jwt_check_accept? is true and the Accept header is present but
24
+ does not match json_request_content_type_regexp.
25
+
26
+ * json_request_content_type_regexp: The regexp to use to recognize
27
+ a request as a json request.
28
+
29
+ * json_response_content_type: The content type to set for json
30
+ responses, application/json by default.
31
+
32
+ * jwt_authorization_ignore: A regexp matched against the
33
+ Authorization header, which skips JWT processing if it matches.
34
+ By default, HTTP Basic and Digest authentication are ignored.
35
+
36
+ * jwt_authorization_remove: A regexp to remove from the
37
+ Authorization header before processing the JWT. By default, a
38
+ Bearer prefix is removed.
39
+
40
+ * jwt_check_accept?: Whether to check the Accept header to see if
41
+ the client supports JSON responses, false by default for backwards
42
+ compatibility.
43
+
44
+ * session_jwt: An encoded JWT for the current session.
45
+
46
+ * use_jwt?: Whether to use the JWT in the Authorization header for
47
+ authentication information. If false, falls back to using the
48
+ rack session. By default, the Authorization header is used if it
49
+ is present, if only_json? is true, or if the request uses a json
50
+ content type.
51
+
52
+ = jwt Feature Backwards Compatibility Issues
53
+
54
+ * The only_json? setting in the jwt feature is now only true by
55
+ default if the :json=>:only option was used when loading the
56
+ rodauth plugin into the roda app. Previously, it was always true,
57
+ but it only was considered in requests to Rodauth endpoints. It
58
+ now also is considered in most Rodauth calls, and if true will use
59
+ an empty session instead of falling back to the rack session if an
60
+ Authorization header is not present.
61
+
62
+ * Previously, the jwt feature only handled requests where the
63
+ request content-type is JSON. It now also handles non-JSON
64
+ requests if the Authorization header is present or if only_json?
65
+ is true.
66
+
67
+ * If an invalid JWT format is used in the Authorization header,
68
+ Rodauth now returns a 400 error, instead of raising an exception.
69
+
70
+ = Other Improvements
71
+
72
+ * A template_opts configuration method has been added, for
73
+ overriding the view/render options. One possible use for this is
74
+ to specify a non-default layout.
@@ -19,6 +19,7 @@ module Rodauth
19
19
  end
20
20
 
21
21
  def self.configure(app, opts={}, &block)
22
+ app.opts[:rodauth_json] = opts.fetch(:json, app.opts[:rodauth_json])
22
23
  ((app.opts[:rodauths] ||= {})[opts[:name]] ||= Class.new(Auth)).configure(&block)
23
24
  end
24
25
 
@@ -29,6 +29,7 @@ module Rodauth
29
29
  auth_value_method :prefix, ''
30
30
  auth_value_method :require_bcrypt?, true
31
31
  auth_value_method :skip_status_checks?, true
32
+ auth_value_method :template_opts, {}
32
33
  auth_value_method :title_instance_variable, nil
33
34
  auth_value_method :unverified_account_message, "unverified account, please verify account before logging in"
34
35
 
@@ -471,14 +472,17 @@ module Rodauth
471
472
  end
472
473
 
473
474
  def _view(meth, page)
474
- auth = self
475
475
  auth_template_path = template_path(page)
476
+ opts = template_opts.dup
477
+ opts[:locals] = opts[:locals] ? opts[:locals].dup : {}
478
+ opts[:locals][:rodauth] = self
479
+
476
480
  scope.instance_exec do
477
- template_opts = find_template(parse_template_opts(page, :locals=>{:rodauth=>auth}))
478
- unless File.file?(template_path(template_opts))
479
- template_opts[:path] = auth_template_path
481
+ opts = find_template(parse_template_opts(page, opts))
482
+ unless File.file?(template_path(opts))
483
+ opts[:path] = auth_template_path
480
484
  end
481
- send(meth, template_opts)
485
+ send(meth, opts)
482
486
  end
483
487
  end
484
488
  end
@@ -4,30 +4,42 @@ require 'jwt'
4
4
 
5
5
  module Rodauth
6
6
  Jwt = Feature.define(:jwt) do
7
+ auth_value_method :invalid_jwt_format_error_message, "invalid JWT format in Authorization header"
7
8
  auth_value_method :json_non_post_error_message, 'non-POST method used in JSON API'
9
+ auth_value_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'
8
13
  auth_value_method :json_response_error_status, 400
9
14
  auth_value_method :json_response_error_key, "error"
10
15
  auth_value_method :json_response_field_error_key, "field-error"
11
16
  auth_value_method :json_response_success_key, nil
12
17
  auth_value_method :jwt_algorithm, "HS256"
18
+ auth_value_method :jwt_authorization_ignore, /\A(?:Basic|Digest) /
19
+ auth_value_method :jwt_authorization_remove, /\ABearer:?\s+/
20
+ auth_value_method :jwt_check_accept?, false
13
21
  auth_value_method :non_json_request_error_message, 'Only JSON format requests are allowed'
14
- auth_value_method :only_json?, true
15
22
 
16
- auth_value_methods :jwt_secret
23
+ auth_value_methods(
24
+ :only_json?,
25
+ :jwt_secret,
26
+ :use_jwt?
27
+ )
17
28
 
18
29
  auth_methods(
19
30
  :json_request?,
20
31
  :jwt_token,
32
+ :session_jwt,
21
33
  :set_jwt_token
22
34
  )
23
35
 
24
36
  def session
25
- return super unless json_request?
26
37
  return @session if defined?(@session)
27
- @session = if token = jwt_token
38
+ return super unless use_jwt?
39
+
40
+ @session = if jwt_token
28
41
  s = {}
29
- payload, header = JWT.decode(token, jwt_secret, true, :algorithm=>jwt_algorithm)
30
- payload.each do |k,v|
42
+ jwt_payload.each do |k,v|
31
43
  s[k.to_sym] = v
32
44
  end
33
45
  s
@@ -38,37 +50,47 @@ module Rodauth
38
50
 
39
51
  def clear_session
40
52
  super
41
- set_jwt if json_request?
53
+ set_jwt if use_jwt?
54
+ end
55
+
56
+ def only_json?
57
+ scope.class.opts[:rodauth_json] == :only
42
58
  end
43
59
 
44
60
  def set_field_error(field, message)
45
- return super unless json_request?
61
+ return super unless use_jwt?
46
62
  json_response[json_response_field_error_key] = [field, message]
47
63
  end
48
64
 
49
65
  def set_error_flash(message)
50
- return super unless json_request?
66
+ return super unless use_jwt?
51
67
  json_response[json_response_error_key] = message
52
68
  end
53
69
 
54
70
  def set_redirect_error_flash(message)
55
- return super unless json_request?
71
+ return super unless use_jwt?
56
72
  json_response[json_response_error_key] = message
57
73
  end
58
74
 
59
75
  def set_notice_flash(message)
60
- return super unless json_request?
76
+ return super unless use_jwt?
61
77
  json_response[json_response_success_key] = message if include_success_messages?
62
78
  end
63
79
 
64
80
  def set_notice_now_flash(message)
65
- return super unless json_request?
81
+ return super unless use_jwt?
66
82
  json_response[json_response_success_key] = message if include_success_messages?
67
83
  end
68
84
 
85
+ def session_jwt
86
+ JWT.encode(session, jwt_secret, jwt_algorithm)
87
+ end
88
+
69
89
  def jwt_token
70
- if v = request.env['HTTP_AUTHORIZATION']
71
- v.sub(/\ABearer:?\s+/, '')
90
+ return @jwt_token if defined?(@jwt_token)
91
+
92
+ if (v = request.env['HTTP_AUTHORIZATION']) && v !~ jwt_authorization_ignore
93
+ @jwt_token = v.sub(jwt_authorization_remove, '')
72
94
  end
73
95
  end
74
96
 
@@ -79,19 +101,27 @@ module Rodauth
79
101
  private
80
102
 
81
103
  def before_rodauth
82
- if only_json? && !json_request?
104
+ if json_request?
105
+ if jwt_check_accept? && (accept = request.env['HTTP_ACCEPT']) && accept !~ json_accept_regexp
106
+ response.status = 406
107
+ json_response[json_response_error_key] = json_not_accepted_error_message
108
+ response['Content-Type'] ||= json_response_content_type
109
+ response.write(request.send(:convert_to_json, json_response))
110
+ request.halt
111
+ end
112
+
113
+ unless request.post?
114
+ response.status = 405
115
+ response.headers['Allow'] = 'POST'
116
+ json_response[json_response_error_key] = json_non_post_error_message
117
+ return_json_response
118
+ end
119
+ elsif only_json?
83
120
  response.status = json_response_error_status
84
121
  response.write non_json_request_error_message
85
122
  request.halt
86
123
  end
87
124
 
88
- if json_request? && !request.post?
89
- response.status = 405
90
- response.headers['Allow'] = 'POST'
91
- json_response[json_response_error_key] = json_non_post_error_message
92
- return_json_response
93
- end
94
-
95
125
  super
96
126
  end
97
127
 
@@ -103,12 +133,22 @@ module Rodauth
103
133
  end
104
134
  end
105
135
 
136
+ def jwt_payload
137
+ JWT.decode(jwt_token, jwt_secret, true, :algorithm=>jwt_algorithm)[0]
138
+ rescue JWT::DecodeError
139
+ json_response[json_response_error_key] = invalid_jwt_format_error_message
140
+ response.status ||= json_response_error_status
141
+ response['Content-Type'] ||= json_response_content_type
142
+ response.write(request.send(:convert_to_json, json_response))
143
+ request.halt
144
+ end
145
+
106
146
  def jwt_secret
107
147
  raise ArgumentError, "jwt_secret not set"
108
148
  end
109
149
 
110
150
  def redirect(_)
111
- return super unless json_request?
151
+ return super unless use_jwt?
112
152
  return_json_response
113
153
  end
114
154
 
@@ -118,7 +158,7 @@ module Rodauth
118
158
 
119
159
  def set_session_value(key, value)
120
160
  super
121
- set_jwt if json_request?
161
+ set_jwt if use_jwt?
122
162
  value
123
163
  end
124
164
 
@@ -127,7 +167,7 @@ module Rodauth
127
167
  end
128
168
 
129
169
  def _view(meth, page)
130
- return super unless json_request?
170
+ return super unless use_jwt?
131
171
  return super if meth == :render
132
172
  return_json_response
133
173
  end
@@ -135,17 +175,22 @@ module Rodauth
135
175
  def return_json_response
136
176
  response.status ||= json_response_error_status if json_response[json_response_error_key]
137
177
  set_jwt
178
+ response['Content-Type'] ||= json_response_content_type
138
179
  response.write(request.send(:convert_to_json, json_response))
139
180
  request.halt
140
181
  end
141
182
 
142
183
  def set_jwt
143
- set_jwt_token(JWT.encode(session, jwt_secret, jwt_algorithm))
184
+ set_jwt_token(session_jwt)
144
185
  end
145
186
 
146
187
  def json_request?
147
188
  return @json_request if defined?(@json_request)
148
- @json_request = request.content_type =~ /application\/json/
189
+ @json_request = request.content_type =~ json_request_content_type_regexp
190
+ end
191
+
192
+ def use_jwt?
193
+ jwt_token || only_json? || json_request?
149
194
  end
150
195
  end
151
196
  end
@@ -1,7 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
3
  module Rodauth
4
- VERSION = '1.4.0'.freeze
4
+ VERSION = '1.5.0'.freeze
5
5
 
6
6
  def self.version
7
7
  VERSION
@@ -0,0 +1,125 @@
1
+ require File.expand_path("spec_helper", File.dirname(__FILE__))
2
+
3
+ describe 'Rodauth login feature' do
4
+ it "should not have jwt feature assume JWT token given during Basic/Digest authentication" do
5
+ rodauth do
6
+ enable :login, :logout
7
+ end
8
+ roda(:jwt) do |r|
9
+ rodauth.require_authentication
10
+ '1'
11
+ end
12
+
13
+ res = json_request("/", :headers=>{'HTTP_AUTHORIZATION'=>'Basic foo'})
14
+ res.must_equal [400, {'error'=>'Please login to continue'}]
15
+
16
+ res = json_request("/", :headers=>{'HTTP_AUTHORIZATION'=>'Digest foo'})
17
+ res.must_equal [400, {'error'=>'Please login to continue'}]
18
+ end
19
+
20
+ it "should return error message if invalid JWT format used in request Authorization header" do
21
+ rodauth do
22
+ enable :login, :logout
23
+ end
24
+ roda(:jwt) do |r|
25
+ r.rodauth
26
+ rodauth.require_authentication
27
+ '1'
28
+ end
29
+
30
+ res = json_request('/login', :include_headers=>true, :login=>'foo@example.com', :password=>'0123456789')
31
+
32
+ res = json_request("/", :headers=>{'HTTP_AUTHORIZATION'=>res[1]['Authorization'][1..-1]})
33
+ res.must_equal [400, {'error'=>'invalid JWT format in Authorization header'}]
34
+ end
35
+
36
+ it "should require json request content type in only json mode for rodauth endpoints only" do
37
+ oj = false
38
+ rodauth do
39
+ enable :login, :logout, :jwt
40
+ jwt_secret '1'
41
+ json_response_success_key 'success'
42
+ only_json?{oj}
43
+ end
44
+ roda(:csrf=>false, :json=>true) do |r|
45
+ r.rodauth
46
+ rodauth.require_authentication
47
+ '1'
48
+ end
49
+
50
+ res = json_request("/", :content_type=>'application/x-www-form-urlencoded', :include_headers=>true, :method=>'GET')
51
+ res[1].delete('Set-Cookie')
52
+ res.must_equal [302, {"Content-Type"=>'text/html', "Content-Length"=>'0', "Location"=>"/login",}, []]
53
+
54
+ res = json_request("/", :content_type=>'application/vnd.api+json', :method=>'GET')
55
+ res.must_equal [400, ['{"error":"Please login to continue"}']]
56
+
57
+ oj = true
58
+
59
+ res = json_request("/", :content_type=>'application/x-www-form-urlencoded', :method=>'GET')
60
+ res.must_equal [400, ['{"error":"Please login to continue"}']]
61
+
62
+ res = json_request("/", :method=>'GET')
63
+ res.must_equal [400, {'error'=>'Please login to continue'}]
64
+
65
+ res = json_request("/login", :content_type=>'application/x-www-form-urlencoded', :include_headers=>true, :method=>'GET')
66
+ msg = "Only JSON format requests are allowed"
67
+ res[1].delete('Set-Cookie')
68
+ res.must_equal [400, {"Content-Type"=>'text/html', "Content-Length"=>msg.length.to_s}, [msg]]
69
+
70
+ json_login
71
+
72
+ res = json_request("/", :content_type=>'application/x-www-form-urlencoded', :include_headers=>true, :method=>'GET')
73
+ res.must_equal [200, {"Content-Type"=>'text/html', "Content-Length"=>'1'}, ['1']]
74
+ end
75
+
76
+ it "should allow non-json requests if only_json? is false" do
77
+ rodauth do
78
+ enable :login, :logout, :jwt
79
+ jwt_secret '1'
80
+ only_json? false
81
+ end
82
+ roda(:jwt_html) do |r|
83
+ r.rodauth
84
+ rodauth.require_authentication
85
+ view(:content=>'1')
86
+ end
87
+
88
+ login
89
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
90
+ end
91
+
92
+ it "should require POST for json requests" do
93
+ rodauth do
94
+ enable :login, :logout, :jwt
95
+ jwt_secret '1'
96
+ json_response_success_key 'success'
97
+ end
98
+ roda(:jwt) do |r|
99
+ r.rodauth
100
+ end
101
+
102
+ res = json_request("/login", :method=>'GET')
103
+ res.must_equal [405, {'error'=>'non-POST method used in JSON API'}]
104
+ end
105
+
106
+ it "should require Accept contain application/json if jwt_check_accept? is true and Accept is present" do
107
+ rodauth do
108
+ enable :login, :logout, :jwt
109
+ jwt_secret '1'
110
+ json_response_success_key 'success'
111
+ jwt_check_accept? true
112
+ end
113
+ roda(:jwt) do |r|
114
+ r.rodauth
115
+ end
116
+
117
+ res = json_request("/login", :headers=>{'HTTP_ACCEPT'=>'text/html'})
118
+ res.must_equal [406, {'error'=>'Unsupported Accept header. Must accept "application/json" or compatible content type'}]
119
+
120
+ json_request("/login", :login=>'foo@example.com', :password=>'0123456789').must_equal [200, {"success"=>'You have been logged in'}]
121
+ json_request("/login", :headers=>{'HTTP_ACCEPT'=>'*/*'}, :login=>'foo@example.com', :password=>'0123456789').must_equal [200, {"success"=>'You have been logged in'}]
122
+ json_request("/login", :headers=>{'HTTP_ACCEPT'=>'application/*'}, :login=>'foo@example.com', :password=>'0123456789').must_equal [200, {"success"=>'You have been logged in'}]
123
+ json_request("/login", :headers=>{'HTTP_ACCEPT'=>'application/vnd.api+json'}, :login=>'foo@example.com', :password=>'0123456789').must_equal [200, {"success"=>'You have been logged in'}]
124
+ end
125
+ end
@@ -161,18 +161,12 @@ describe 'Rodauth login feature' do
161
161
  end
162
162
  roda(:jwt) do |r|
163
163
  r.rodauth
164
+ response['Content-Type'] = 'application/json'
164
165
  rodauth.logged_in? ? '1' : '2'
165
166
  end
166
167
 
167
168
  json_request.must_equal [200, 2]
168
169
 
169
- visit '/login'
170
- page.status_code.must_equal 400
171
- page.body.must_equal "Only JSON format requests are allowed"
172
-
173
- res = json_request("/login", :method=>'GET')
174
- res.must_equal [405, {'error'=>'non-POST method used in JSON API'}]
175
-
176
170
  res = json_request("/login", :login=>'foo@example2.com', :password=>'0123456789')
177
171
  res.must_equal [400, {'error'=>"There was an error logging in", "field-error"=>["login", "no matching login"]}]
178
172
 
@@ -12,6 +12,19 @@ describe 'Rodauth' do
12
12
  proc{visit '/'}.must_raise NoMethodError
13
13
  end
14
14
 
15
+ it "should support template_opts" do
16
+ rodauth do
17
+ enable :login
18
+ template_opts(:layout_opts=>{:path=>'spec/views/layout-other.str'})
19
+ end
20
+ roda do |r|
21
+ r.rodauth
22
+ end
23
+
24
+ visit '/login'
25
+ page.title.must_equal 'Foo Login'
26
+ end
27
+
15
28
  it "should require login to perform certain actions" do
16
29
  rodauth do
17
30
  enable :login, :change_password, :change_login, :close_account
@@ -57,7 +57,7 @@ ENV['RACK_ENV'] = 'test'
57
57
  end
58
58
 
59
59
  Base = Class.new(Roda)
60
- Base.plugin :render, :layout=>{:path=>'spec/views/layout.str'}
60
+ Base.plugin :render, :layout_opts=>{:path=>'spec/views/layout.str'}
61
61
  Base.plugin(:not_found){raise "path #{request.path_info} not found"}
62
62
  Base.use Rack::Session::Cookie, :secret=>'0123456789'
63
63
  class Base
@@ -86,11 +86,17 @@ class Minitest::HooksSpec
86
86
  end
87
87
 
88
88
  def roda(type=nil, &block)
89
- jwt = type == :jwt
90
- app = Class.new(jwt ? JsonBase : Base)
89
+ jwt_only = type == :jwt
90
+ jwt = type == :jwt || type == :jwt_html
91
+
92
+ app = Class.new(jwt_only ? JsonBase : Base)
91
93
  rodauth_block = @rodauth_block
92
94
  opts = type.is_a?(Hash) ? type : {}
93
- opts[:json] = :only if jwt
95
+
96
+ if jwt
97
+ opts[:json] = jwt_only ? :only : true
98
+ end
99
+
94
100
  app.plugin(:rodauth, opts) do
95
101
  title_instance_variable :@title
96
102
  if jwt
@@ -129,6 +135,9 @@ class Minitest::HooksSpec
129
135
  end
130
136
 
131
137
  def json_request(path='/', params={})
138
+ include_headers = params.delete(:include_headers)
139
+ headers = params.delete(:headers)
140
+
132
141
  env = {"REQUEST_METHOD" => params.delete(:method) || "POST",
133
142
  "PATH_INFO" => path,
134
143
  "SCRIPT_NAME" => "",
@@ -144,6 +153,8 @@ class Minitest::HooksSpec
144
153
  env["HTTP_COOKIE"] = @cookie
145
154
  end
146
155
 
156
+ env.merge!(headers) if headers
157
+
147
158
  r = @app.call(env)
148
159
 
149
160
  if cookie = r[1]['Set-Cookie']
@@ -152,7 +163,14 @@ class Minitest::HooksSpec
152
163
  if authorization = r[1]['Authorization']
153
164
  @authorization = authorization
154
165
  end
155
- [r[0], JSON.parse("[#{r[2].join}]").first]
166
+
167
+ if env["CONTENT_TYPE"] == "application/json"
168
+ r[1]['Content-Type'].must_equal 'application/json'
169
+ r[2] = JSON.parse("[#{r[2].join}]").first
170
+ end
171
+
172
+ r.delete_at(1) unless include_headers
173
+ r
156
174
  end
157
175
 
158
176
  def json_login(opts={})
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Foo #{@title}</title>
5
+ </head>
6
+ <body>
7
+ #{"<div id='error_flash'>#{flash[:error]}</div>" if flash[:error]}
8
+ #{"<div id='notice_flash'>#{flash[:notice]}</div>" if flash[:notice]}
9
+ #{yield}
10
+ </body>
11
+ </html>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-08-18 00:00:00.000000000 Z
11
+ date: 2016-09-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -230,6 +230,7 @@ extra_rdoc_files:
230
230
  - doc/release_notes/1.2.0.txt
231
231
  - doc/release_notes/1.3.0.txt
232
232
  - doc/release_notes/1.4.0.txt
233
+ - doc/release_notes/1.5.0.txt
233
234
  files:
234
235
  - CHANGELOG
235
236
  - MIT-LICENSE
@@ -259,6 +260,7 @@ files:
259
260
  - doc/release_notes/1.2.0.txt
260
261
  - doc/release_notes/1.3.0.txt
261
262
  - doc/release_notes/1.4.0.txt
263
+ - doc/release_notes/1.5.0.txt
262
264
  - doc/remember.rdoc
263
265
  - doc/reset_password.rdoc
264
266
  - doc/session_expiration.rdoc
@@ -310,6 +312,7 @@ files:
310
312
  - spec/confirm_password_spec.rb
311
313
  - spec/create_account_spec.rb
312
314
  - spec/disallow_password_reuse_spec.rb
315
+ - spec/jwt_spec.rb
313
316
  - spec/lockout_spec.rb
314
317
  - spec/login_spec.rb
315
318
  - spec/migrate/001_tables.rb
@@ -330,6 +333,7 @@ files:
330
333
  - spec/verify_account_grace_period_spec.rb
331
334
  - spec/verify_account_spec.rb
332
335
  - spec/verify_change_login_spec.rb
336
+ - spec/views/layout-other.str
333
337
  - spec/views/layout.str
334
338
  - spec/views/login.str
335
339
  - templates/add-recovery-codes.str