rodauth 1.5.0 → 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +18 -0
- data/README.rdoc +11 -9
- data/doc/base.rdoc +7 -0
- data/doc/http_basic_auth.rdoc +8 -0
- data/doc/jwt.rdoc +6 -2
- data/doc/login.rdoc +0 -7
- data/doc/release_notes/1.6.0.txt +37 -0
- data/lib/rodauth/features/base.rb +8 -0
- data/lib/rodauth/features/http_basic_auth.rb +50 -0
- data/lib/rodauth/features/jwt.rb +18 -14
- data/lib/rodauth/features/login.rb +0 -4
- data/lib/rodauth/features/reset_password.rb +3 -1
- data/lib/rodauth/version.rb +1 -1
- data/spec/http_basic_auth_spec.rb +125 -0
- data/spec/jwt_spec.rb +59 -1
- data/spec/reset_password_spec.rb +3 -0
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8be62d375631a0d62337cc9da83782e1a765ecc1
|
4
|
+
data.tar.gz: e5cc52f5bec8e0f7820b1b16ac11d96d385e0187
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cd373d957b4b636afab06b24a7c782c9b3e605de844f0083c7eb2953779715dc4fec20418984ef4cdbce93d9f65e4848eed11a8318985d52d03ecdb9b2f1301f
|
7
|
+
data.tar.gz: d156aaad0e89d164a467e44c946088f28487db41a4b0cb1ec57dda6584ca36ffbb8c87daf02bcc868dcfe8322bf56fc4fa005ab9fdfd2cd0b8620bce6334d593
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,21 @@
|
|
1
|
+
=== 1.6.0 (2016-10-24)
|
2
|
+
|
3
|
+
* Add http_basic_auth feature (TiagoCardoso1983, jeremyevans) (#12)
|
4
|
+
|
5
|
+
* Move login hooks from login feature to base, to be usable by other features (jeremyevans)
|
6
|
+
|
7
|
+
* Make reset_password feature not attempt to render a template in json-only mode (jeremyevans) (#11)
|
8
|
+
|
9
|
+
* Memoize jwt_payload in jwt feature, as it may be called more than once (mwpastore) (#10)
|
10
|
+
|
11
|
+
* Add jwt_decode_opts configuration method to jwt feature, for specifying options to JWT.decode, allowing for JWT claim verification (mwpastore, jeremyevans) (#9)
|
12
|
+
|
13
|
+
* Add jwt_session_hash configuration method to jwt feature, for modifying the session information stored in the JWT hash, allowing for setting JWT claims (mwpastore, jeremyevans) (#9)
|
14
|
+
|
15
|
+
* Add jwt_session_key configuration method to jwt feature, for nesting the session under a key in the JWT, avoiding reserve claim names (mwpastore, jeremyevans) (#9)
|
16
|
+
|
17
|
+
* Add jwt_symbolize_deeply? configuration method to jwt feature, for symbolizing nested keys in session hash when using JWT (mwpastore) (#9)
|
18
|
+
|
1
19
|
=== 1.5.0 (2016-09-22)
|
2
20
|
|
3
21
|
* Return error instead of raising exception in the jwt feature if an invalid jwt format is submitted in the Authorization header (jeremyevans)
|
data/README.rdoc
CHANGED
@@ -40,6 +40,7 @@ hashes by protecting access via database functions.
|
|
40
40
|
* Single Session (Only one active session per account)
|
41
41
|
* JWT (JSON API support for all other features)
|
42
42
|
* Update Password Hash (when hash cost changes)
|
43
|
+
* HTTP Basic Auth
|
43
44
|
|
44
45
|
== Resources
|
45
46
|
|
@@ -505,7 +506,7 @@ add additional logging when a user logs in:
|
|
505
506
|
plugin :rodauth do
|
506
507
|
enable :login, :logout
|
507
508
|
after_login do
|
508
|
-
LOGGER.info "#{account
|
509
|
+
LOGGER.info "#{account[:email]} logged in!"
|
509
510
|
end
|
510
511
|
end
|
511
512
|
|
@@ -525,7 +526,7 @@ So if you want to log the IP address for the user during login:
|
|
525
526
|
plugin :rodauth do
|
526
527
|
enable :login, :logout
|
527
528
|
after_login do
|
528
|
-
LOGGER.info "#{account
|
529
|
+
LOGGER.info "#{account[:email]} logged in from #{request.ip}"
|
529
530
|
end
|
530
531
|
end
|
531
532
|
|
@@ -836,14 +837,15 @@ By setting <tt>env['rodauth'] = rodauth</tt> in the route block
|
|
836
837
|
inside the middleware, you can easily provide a way for your
|
837
838
|
application to call Rodauth methods.
|
838
839
|
|
839
|
-
|
840
|
-
doesn't use Roda
|
841
|
-
{this example integrating Rodauth into Ginatra, a Sinatra-based git repository viewer}[https://github.com/jeremyevans/ginatra/commit/28108ebec96e8d42596ee55b01c3f7b50c155dd1].
|
840
|
+
Here are some examples of integrating Rodauth into applications that
|
841
|
+
doesn't use Roda:
|
842
842
|
|
843
|
-
|
844
|
-
{
|
845
|
-
This uses the {roda-rails gem}[https://github.com/jeremyevans/roda-rails]
|
846
|
-
so that Rodauth uses Rails' CSRF and flash support
|
843
|
+
* {Ginatra, a Sinatra-based git repository viewer}[https://github.com/jeremyevans/ginatra/commit/28108ebec96e8d42596ee55b01c3f7b50c155dd1]
|
844
|
+
* {Rodauth's demo site as a Rails application}[https://github.com/jeremyevans/rodauth-demo-rails] (
|
845
|
+
This uses the {roda-rails gem}[https://github.com/jeremyevans/roda-rails]
|
846
|
+
so that Rodauth uses Rails' CSRF and flash support)
|
847
|
+
* {Grape application}[https://github.com/davydovanton/grape-rodauth]
|
848
|
+
* {Hanami application}[https://github.com/davydovanton/rodauth_hanami]
|
847
849
|
|
848
850
|
=== Using 2 Factor Authentication
|
849
851
|
|
data/doc/base.rdoc
CHANGED
@@ -69,6 +69,13 @@ use_database_authentication_functions? :: Whether to use functions to do authent
|
|
69
69
|
|
70
70
|
== Auth Methods
|
71
71
|
|
72
|
+
after_login :: Run arbitrary code after a successful login.
|
73
|
+
after_login_failure :: Run arbitrary code after a login failure due to
|
74
|
+
an invalid password.
|
75
|
+
before_login :: Run arbitrary code after password has been checked, but
|
76
|
+
before updating the session.
|
77
|
+
before_login_attempt :: Run arbitrary code after an account has been
|
78
|
+
located, but before the password has been checked.
|
72
79
|
before_rodauth :: Run arbitrary code before handling any rodauth route.
|
73
80
|
account_from_login(login) :: Retrieve the account model instance related to the
|
74
81
|
given login or nil if no login matches.
|
data/doc/jwt.rdoc
CHANGED
@@ -46,17 +46,21 @@ json_response_error_status :: The HTTP status code to use for JSON error respons
|
|
46
46
|
json_response_field_error_key :: The JSON result key containing an field error message, "field-error" by default.
|
47
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.
|
48
48
|
jwt_algorithm :: The JWT algorithm to use, "HS256" by default.
|
49
|
-
non_json_request_error_message :: The error message to use when a non-JSON request is sent and +only_json?+ is set.
|
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
49
|
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
50
|
jwt_authorization_remove :: A regexp to remove from the Authorization header before processing the JWT. By default, a Bearer prefix is removed.
|
53
51
|
jwt_check_accept? :: Whether to check the Accept header to see if the client supports JSON responses, false by default for backwards compatibility.
|
52
|
+
jwt_decode_opts :: An optional hash to pass to JWT.decode. Can be used to set JWT verifiers.
|
54
53
|
jwt_secret :: The JWT secret to use. Access to this should be protected the same as a session secret.
|
54
|
+
jwt_session_key :: A key to nest the session hash under in the JWT payload. nil by default, for no nesting.
|
55
|
+
jwt_symbolize_deeply? :: Whether to symbolize the session hash deeply. false by default.
|
56
|
+
non_json_request_error_message :: The error message to use when a non-JSON request is sent and +only_json?+ is set.
|
57
|
+
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.
|
55
58
|
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.
|
56
59
|
|
57
60
|
== Auth Methods
|
58
61
|
|
59
62
|
json_request? :: Whether the current request is a JSON request, looks at the Content-Type request header by default.
|
63
|
+
jwt_session_hash :: The session hash used to create the session_jwt. Can be used to set JWT claims.
|
60
64
|
jwt_token :: Retrieve the JWT token from the request, by default taking it from the Authorization header.
|
61
65
|
session_jwt :: An encoded JWT for the current session.
|
62
66
|
set_jwt_token(token) :: Set the JWT token in the response, by default storing it in the Authorization header.
|
data/doc/login.rdoc
CHANGED
@@ -16,12 +16,5 @@ login_route :: The route to the login action.
|
|
16
16
|
|
17
17
|
== Auth Methods
|
18
18
|
|
19
|
-
after_login :: Run arbitrary code after a successful login.
|
20
|
-
after_login_failure :: Run arbitrary code after a login failure due to
|
21
|
-
an invalid password.
|
22
|
-
before_login :: Run arbitrary code after password has been checked, but
|
23
|
-
before updating the session.
|
24
|
-
before_login_attempt :: Run arbitrary code after an account has been
|
25
|
-
located, but before the password has been checked.
|
26
19
|
before_login_route :: Run arbitrary code before handling a login route.
|
27
20
|
login_view :: The HTML to use for the login form.
|
@@ -0,0 +1,37 @@
|
|
1
|
+
= New Feature
|
2
|
+
|
3
|
+
* An http_basic_auth feature has been added, allowing the use of
|
4
|
+
HTTP Basic Auth to login.
|
5
|
+
|
6
|
+
= New Configuration Options for jwt Feature
|
7
|
+
|
8
|
+
* jwt_session_hash has been added, for modifying the hash given before
|
9
|
+
creating the JWT. This can be used for setting JWT claims.
|
10
|
+
Example:
|
11
|
+
|
12
|
+
jwt_session_hash do
|
13
|
+
super().merge(:exp=>Time.now.to_i + 120)
|
14
|
+
end
|
15
|
+
|
16
|
+
* jwt_decode_opts has been added for specifying additional options to
|
17
|
+
JWT.decode. Among other things, this allows for JWT claim
|
18
|
+
verification. Example:
|
19
|
+
|
20
|
+
jwt_decode_opts(:verify_expiration=>true)
|
21
|
+
|
22
|
+
* jwt_session_key has been added, specifying a key in the JWT that
|
23
|
+
will be used to store session information, instead of storing
|
24
|
+
session keys in the root of the JWT. Use of this option can avoid
|
25
|
+
issues with reserved JWT claim names, and will probably be enabled
|
26
|
+
by default starting in Rodauth 2.
|
27
|
+
|
28
|
+
* jwt_symbolize_deeply? configuration method has been added, for
|
29
|
+
whether to symbolize nested keys when decoding a JWT session hash.
|
30
|
+
|
31
|
+
= Other Improvements
|
32
|
+
|
33
|
+
* The reset_password feature no longer attempts to render a template
|
34
|
+
in json-only mode.
|
35
|
+
|
36
|
+
* The jwt_payload method is now memoized by default.
|
37
|
+
|
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
module Rodauth
|
4
4
|
Base = Feature.define(:base) do
|
5
|
+
after 'login'
|
6
|
+
after 'login_failure'
|
7
|
+
before 'login'
|
8
|
+
before 'login_attempt'
|
5
9
|
before 'rodauth'
|
6
10
|
|
7
11
|
error_flash "Please login to continue", 'require_login'
|
@@ -382,6 +386,10 @@ module Rodauth
|
|
382
386
|
{account_status_column=>account_open_status_value}
|
383
387
|
end
|
384
388
|
|
389
|
+
def only_json?
|
390
|
+
scope.class.opts[:rodauth_json] == :only
|
391
|
+
end
|
392
|
+
|
385
393
|
def template_path(page)
|
386
394
|
File.join(File.dirname(__FILE__), '../../../templates', "#{page}.str")
|
387
395
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
module Rodauth
|
4
|
+
HTTTBasicAuth = Feature.define(:http_basic_auth) do
|
5
|
+
auth_value_method :http_basic_auth_realm, "protected"
|
6
|
+
|
7
|
+
def session
|
8
|
+
return @session if defined?(@session)
|
9
|
+
sess = super
|
10
|
+
return sess if sess[session_key]
|
11
|
+
return sess unless token = ((v = request.env['HTTP_AUTHORIZATION']) && v[/\A *Basic (.*)\Z/, 1])
|
12
|
+
username, password = token.unpack("m*").first.split(/:/, 2)
|
13
|
+
|
14
|
+
if username && password
|
15
|
+
catch_error do
|
16
|
+
unless account_from_login(username)
|
17
|
+
throw_basic_auth_error(login_param, no_matching_login_message)
|
18
|
+
end
|
19
|
+
|
20
|
+
before_login_attempt
|
21
|
+
|
22
|
+
unless open_account?
|
23
|
+
throw_basic_auth_error(login_param, no_matching_login_message)
|
24
|
+
end
|
25
|
+
|
26
|
+
unless password_match?(password)
|
27
|
+
after_login_failure
|
28
|
+
throw_basic_auth_error(password_param, invalid_password_message)
|
29
|
+
end
|
30
|
+
|
31
|
+
transaction do
|
32
|
+
before_login
|
33
|
+
sess[session_key] = account_session_value
|
34
|
+
after_login
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
sess
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def throw_basic_auth_error(*args)
|
45
|
+
response.status = 401
|
46
|
+
response.headers["WWW-Authenticate"] = "Basic realm=\"#{http_basic_auth_realm}\""
|
47
|
+
throw_error(*args)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/rodauth/features/jwt.rb
CHANGED
@@ -4,7 +4,7 @@ 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
|
+
auth_value_method :invalid_jwt_format_error_message, "invalid JWT format or claim in Authorization header"
|
8
8
|
auth_value_method :json_non_post_error_message, 'non-POST method used in JSON API'
|
9
9
|
auth_value_method :json_not_accepted_error_message, 'Unsupported Accept header. Must accept "application/json" or compatible content type'
|
10
10
|
auth_value_method :json_accept_regexp, /(?:(?:\*|\bapplication)\/\*|\bapplication\/(?:vnd\.api\+)?json\b)/i
|
@@ -18,6 +18,9 @@ module Rodauth
|
|
18
18
|
auth_value_method :jwt_authorization_ignore, /\A(?:Basic|Digest) /
|
19
19
|
auth_value_method :jwt_authorization_remove, /\ABearer:?\s+/
|
20
20
|
auth_value_method :jwt_check_accept?, false
|
21
|
+
auth_value_method :jwt_decode_opts, {}
|
22
|
+
auth_value_method :jwt_session_key, nil
|
23
|
+
auth_value_method :jwt_symbolize_deeply?, false
|
21
24
|
auth_value_method :non_json_request_error_message, 'Only JSON format requests are allowed'
|
22
25
|
|
23
26
|
auth_value_methods(
|
@@ -28,6 +31,7 @@ module Rodauth
|
|
28
31
|
|
29
32
|
auth_methods(
|
30
33
|
:json_request?,
|
34
|
+
:jwt_session_hash,
|
31
35
|
:jwt_token,
|
32
36
|
:session_jwt,
|
33
37
|
:set_jwt_token
|
@@ -37,15 +41,15 @@ module Rodauth
|
|
37
41
|
return @session if defined?(@session)
|
38
42
|
return super unless use_jwt?
|
39
43
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
s
|
44
|
+
s = {}
|
45
|
+
if jwt_token && (session_data = jwt_session_key ? jwt_payload[jwt_session_key] : jwt_payload)
|
46
|
+
if jwt_symbolize_deeply?
|
47
|
+
s = JSON.parse(JSON.fast_generate(session_data), :symbolize_names=>true)
|
48
|
+
else
|
49
|
+
session_data.each{|k,v| s[k.to_sym] = v}
|
44
50
|
end
|
45
|
-
s
|
46
|
-
else
|
47
|
-
{}
|
48
51
|
end
|
52
|
+
@session = s
|
49
53
|
end
|
50
54
|
|
51
55
|
def clear_session
|
@@ -53,10 +57,6 @@ module Rodauth
|
|
53
57
|
set_jwt if use_jwt?
|
54
58
|
end
|
55
59
|
|
56
|
-
def only_json?
|
57
|
-
scope.class.opts[:rodauth_json] == :only
|
58
|
-
end
|
59
|
-
|
60
60
|
def set_field_error(field, message)
|
61
61
|
return super unless use_jwt?
|
62
62
|
json_response[json_response_field_error_key] = [field, message]
|
@@ -82,8 +82,12 @@ module Rodauth
|
|
82
82
|
json_response[json_response_success_key] = message if include_success_messages?
|
83
83
|
end
|
84
84
|
|
85
|
+
def jwt_session_hash
|
86
|
+
jwt_session_key ? {jwt_session_key=>session} : session
|
87
|
+
end
|
88
|
+
|
85
89
|
def session_jwt
|
86
|
-
JWT.encode(
|
90
|
+
JWT.encode(jwt_session_hash, jwt_secret, jwt_algorithm)
|
87
91
|
end
|
88
92
|
|
89
93
|
def jwt_token
|
@@ -134,7 +138,7 @@ module Rodauth
|
|
134
138
|
end
|
135
139
|
|
136
140
|
def jwt_payload
|
137
|
-
JWT.decode(jwt_token, jwt_secret, true, :algorithm=>jwt_algorithm)[0]
|
141
|
+
@jwt_payload ||= JWT.decode(jwt_token, jwt_secret, true, jwt_decode_opts.merge(:algorithm=>jwt_algorithm))[0]
|
138
142
|
rescue JWT::DecodeError
|
139
143
|
json_response[json_response_error_key] = invalid_jwt_format_error_message
|
140
144
|
response.status ||= json_response_error_status
|
@@ -165,7 +165,9 @@ module Rodauth
|
|
165
165
|
attr_reader :reset_password_key_value
|
166
166
|
|
167
167
|
def after_login_failure
|
168
|
-
|
168
|
+
unless only_json?
|
169
|
+
@login_form_header = render("reset-password-request")
|
170
|
+
end
|
169
171
|
super
|
170
172
|
end
|
171
173
|
|
data/lib/rodauth/version.rb
CHANGED
@@ -0,0 +1,125 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
describe "Rodauth http basic auth feature" do
|
4
|
+
def basic_auth_visit(opts={})
|
5
|
+
page.driver.browser.basic_authorize(opts.fetch(:username,"foo@example.com"), opts.fetch(:password, "0123456789"))
|
6
|
+
visit(opts.fetch(:path, '/'))
|
7
|
+
end
|
8
|
+
|
9
|
+
def authorization_header(opts={})
|
10
|
+
["#{opts.delete(:username)||'foo@example.com'}:#{opts.delete(:password)||'0123456789'}"].pack("m*")
|
11
|
+
end
|
12
|
+
|
13
|
+
def basic_auth_json_request(opts={})
|
14
|
+
auth = opts.delete(:auth) || authorization_header(opts)
|
15
|
+
path = opts.delete(:path) || '/'
|
16
|
+
json_request(path, opts.merge(:headers => {"HTTP_AUTHORIZATION" => "Basic #{auth}"}, :method=>'GET'))
|
17
|
+
end
|
18
|
+
|
19
|
+
def newline_basic_auth_json_request(opts={})
|
20
|
+
auth = opts.delete(:auth) || authorization_header(opts)
|
21
|
+
auth.chomp!
|
22
|
+
basic_auth_json_request(opts.merge(:auth => auth))
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "on page visit" do
|
26
|
+
before do
|
27
|
+
rodauth do
|
28
|
+
enable :http_basic_auth
|
29
|
+
end
|
30
|
+
roda do |r|
|
31
|
+
r.rodauth
|
32
|
+
r.root{view :content=>(rodauth.logged_in? ? "Logged In" : 'Not Logged')}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it "handles logins" do
|
37
|
+
basic_auth_visit
|
38
|
+
page.text.must_include "Logged In"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "keeps the user logged in" do
|
42
|
+
visit '/'
|
43
|
+
page.text.must_include "Not Logged"
|
44
|
+
|
45
|
+
basic_auth_visit
|
46
|
+
page.text.must_include "Logged In"
|
47
|
+
|
48
|
+
visit '/'
|
49
|
+
page.text.must_include "Logged In"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "fails when no login is found" do
|
53
|
+
basic_auth_visit(:username => "foo2@example.com")
|
54
|
+
page.text.must_include "Not Logged"
|
55
|
+
page.response_headers.keys.must_include("WWW-Authenticate")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "fails when passowrd does not match" do
|
59
|
+
basic_auth_visit(:password => "1111111111")
|
60
|
+
page.text.must_include "Not Logged"
|
61
|
+
page.response_headers.keys.must_include("WWW-Authenticate")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it "works with standard authentication" do
|
66
|
+
rodauth do
|
67
|
+
enable :login, :http_basic_auth
|
68
|
+
end
|
69
|
+
roda do |r|
|
70
|
+
r.rodauth
|
71
|
+
r.root{view :content=>(rodauth.logged_in? ? "Logged In" : 'Not Logged')}
|
72
|
+
end
|
73
|
+
|
74
|
+
login
|
75
|
+
page.text.must_include "Logged In"
|
76
|
+
end
|
77
|
+
|
78
|
+
it "does not allow login to unverified account" do
|
79
|
+
rodauth do
|
80
|
+
enable :http_basic_auth
|
81
|
+
skip_status_checks? false
|
82
|
+
end
|
83
|
+
roda do |r|
|
84
|
+
r.rodauth
|
85
|
+
r.root{view :content=>(rodauth.logged_in? ? "Logged In" : 'Not Logged')}
|
86
|
+
end
|
87
|
+
DB[:accounts].update(:status_id=>1)
|
88
|
+
|
89
|
+
basic_auth_visit
|
90
|
+
page.text.must_include "Not Logged"
|
91
|
+
page.response_headers.keys.must_include("WWW-Authenticate")
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should login via jwt" do
|
95
|
+
rodauth do
|
96
|
+
enable :http_basic_auth
|
97
|
+
end
|
98
|
+
roda(:jwt) do |r|
|
99
|
+
r.rodauth
|
100
|
+
response['Content-Type'] = 'application/json'
|
101
|
+
rodauth.require_authentication
|
102
|
+
{"success"=>'You have been logged in'}
|
103
|
+
end
|
104
|
+
|
105
|
+
@authorization = nil
|
106
|
+
res = basic_auth_json_request(:auth=>'.')
|
107
|
+
res.must_equal [400, {'error'=>"Please login to continue"}]
|
108
|
+
|
109
|
+
@authorization = nil
|
110
|
+
res = basic_auth_json_request(:username=>'foo@example2.com')
|
111
|
+
res.must_equal [401, {'error'=>"Please login to continue", "field-error"=>["login", "no matching login"]}]
|
112
|
+
|
113
|
+
@authorization = nil
|
114
|
+
res = basic_auth_json_request(:password=>'012345678')
|
115
|
+
res.must_equal [401, {'error'=>"Please login to continue", "field-error"=>["password", "invalid password"]}]
|
116
|
+
|
117
|
+
@authorization = nil
|
118
|
+
res = newline_basic_auth_json_request
|
119
|
+
res.must_equal [200, {"success"=>'You have been logged in'}]
|
120
|
+
|
121
|
+
@authorization = nil
|
122
|
+
res = basic_auth_json_request
|
123
|
+
res.must_equal [200, {"success"=>'You have been logged in'}]
|
124
|
+
end
|
125
|
+
end
|
data/spec/jwt_spec.rb
CHANGED
@@ -30,7 +30,7 @@ describe 'Rodauth login feature' do
|
|
30
30
|
res = json_request('/login', :include_headers=>true, :login=>'foo@example.com', :password=>'0123456789')
|
31
31
|
|
32
32
|
res = json_request("/", :headers=>{'HTTP_AUTHORIZATION'=>res[1]['Authorization'][1..-1]})
|
33
|
-
res.must_equal [400, {'error'=>'invalid JWT format in Authorization header'}]
|
33
|
+
res.must_equal [400, {'error'=>'invalid JWT format or claim in Authorization header'}]
|
34
34
|
end
|
35
35
|
|
36
36
|
it "should require json request content type in only json mode for rodauth endpoints only" do
|
@@ -122,4 +122,62 @@ describe 'Rodauth login feature' do
|
|
122
122
|
json_request("/login", :headers=>{'HTTP_ACCEPT'=>'application/*'}, :login=>'foo@example.com', :password=>'0123456789').must_equal [200, {"success"=>'You have been logged in'}]
|
123
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
124
|
end
|
125
|
+
|
126
|
+
it "generates and verifies JWTs with claims" do
|
127
|
+
invalid_jti = false
|
128
|
+
|
129
|
+
rodauth do
|
130
|
+
enable :login, :logout, :jwt
|
131
|
+
jwt_secret '1'
|
132
|
+
json_response_success_key 'success'
|
133
|
+
jwt_session_key 'data'
|
134
|
+
jwt_symbolize_deeply? true
|
135
|
+
jwt_session_hash do
|
136
|
+
h = super()
|
137
|
+
h['data']['foo'] = {:bar=>[1]}
|
138
|
+
h.merge(
|
139
|
+
:aud => %w[Young Old],
|
140
|
+
:exp => Time.now.to_i + 120,
|
141
|
+
:iat => Time.now.to_i,
|
142
|
+
:iss => "Foobar, Inc.",
|
143
|
+
:jti => SecureRandom.hex(10),
|
144
|
+
:nbf => Time.now.to_i - 30,
|
145
|
+
:sub => session_value
|
146
|
+
)
|
147
|
+
end
|
148
|
+
jwt_decode_opts(
|
149
|
+
:aud => 'Old',
|
150
|
+
:iss => "Foobar, Inc.",
|
151
|
+
:leeway => 30,
|
152
|
+
:verify_aud => true,
|
153
|
+
:verify_expiration => true,
|
154
|
+
:verify_iat => true,
|
155
|
+
:verify_iss => true,
|
156
|
+
:verify_jti => proc{|jti| invalid_jti ? false : !!jti},
|
157
|
+
:verify_not_before => true
|
158
|
+
)
|
159
|
+
end
|
160
|
+
roda(:jwt) do |r|
|
161
|
+
r.rodauth
|
162
|
+
r.post{rodauth.session[:foo][:bar]}
|
163
|
+
end
|
164
|
+
|
165
|
+
json_login.must_equal [200, {"success"=>'You have been logged in'}]
|
166
|
+
|
167
|
+
payload = JWT.decode(@authorization, nil, false)[0]
|
168
|
+
payload['sub'].must_equal payload['data']['account_id']
|
169
|
+
payload['iat'].must_be_kind_of Integer
|
170
|
+
payload['exp'].must_be_kind_of Integer
|
171
|
+
payload['nbf'].must_be_kind_of Integer
|
172
|
+
payload['iss'].must_equal "Foobar, Inc."
|
173
|
+
payload['aud'].must_equal %w[Young Old]
|
174
|
+
payload['jti'].must_match(/^[0-9a-f]{20}$/)
|
175
|
+
|
176
|
+
json_request.must_equal [200, [1]]
|
177
|
+
|
178
|
+
invalid_jti = true
|
179
|
+
if RUBY_VERSION >= '1.9'
|
180
|
+
json_login(:no_check=>true).must_equal [400, {"error"=>'invalid JWT format or claim in Authorization header'}]
|
181
|
+
end
|
182
|
+
end
|
125
183
|
end
|
data/spec/reset_password_spec.rb
CHANGED
@@ -155,6 +155,9 @@ describe 'Rodauth reset_password feature' do
|
|
155
155
|
r.rodauth
|
156
156
|
end
|
157
157
|
|
158
|
+
res = json_login(:pass=>'1', :no_check=>true)
|
159
|
+
res.must_equal [400, {"field-error"=>["password", "invalid password"], "error"=>"There was an error logging in"}]
|
160
|
+
|
158
161
|
res = json_request('/reset-password')
|
159
162
|
res.must_equal [400, {"error"=>"There was an error resetting your password"}]
|
160
163
|
|
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
|
+
version: 1.6.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-
|
11
|
+
date: 2016-10-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sequel
|
@@ -225,12 +225,14 @@ extra_rdoc_files:
|
|
225
225
|
- doc/confirm_password.rdoc
|
226
226
|
- doc/verify_change_login.rdoc
|
227
227
|
- doc/update_password_hash.rdoc
|
228
|
+
- doc/http_basic_auth.rdoc
|
228
229
|
- doc/release_notes/1.0.0.txt
|
229
230
|
- doc/release_notes/1.1.0.txt
|
230
231
|
- doc/release_notes/1.2.0.txt
|
231
232
|
- doc/release_notes/1.3.0.txt
|
232
233
|
- doc/release_notes/1.4.0.txt
|
233
234
|
- doc/release_notes/1.5.0.txt
|
235
|
+
- doc/release_notes/1.6.0.txt
|
234
236
|
files:
|
235
237
|
- CHANGELOG
|
236
238
|
- MIT-LICENSE
|
@@ -245,6 +247,7 @@ files:
|
|
245
247
|
- doc/create_account.rdoc
|
246
248
|
- doc/disallow_password_reuse.rdoc
|
247
249
|
- doc/email_base.rdoc
|
250
|
+
- doc/http_basic_auth.rdoc
|
248
251
|
- doc/jwt.rdoc
|
249
252
|
- doc/lockout.rdoc
|
250
253
|
- doc/login.rdoc
|
@@ -261,6 +264,7 @@ files:
|
|
261
264
|
- doc/release_notes/1.3.0.txt
|
262
265
|
- doc/release_notes/1.4.0.txt
|
263
266
|
- doc/release_notes/1.5.0.txt
|
267
|
+
- doc/release_notes/1.6.0.txt
|
264
268
|
- doc/remember.rdoc
|
265
269
|
- doc/reset_password.rdoc
|
266
270
|
- doc/session_expiration.rdoc
|
@@ -282,6 +286,7 @@ files:
|
|
282
286
|
- lib/rodauth/features/create_account.rb
|
283
287
|
- lib/rodauth/features/disallow_password_reuse.rb
|
284
288
|
- lib/rodauth/features/email_base.rb
|
289
|
+
- lib/rodauth/features/http_basic_auth.rb
|
285
290
|
- lib/rodauth/features/jwt.rb
|
286
291
|
- lib/rodauth/features/lockout.rb
|
287
292
|
- lib/rodauth/features/login.rb
|
@@ -312,6 +317,7 @@ files:
|
|
312
317
|
- spec/confirm_password_spec.rb
|
313
318
|
- spec/create_account_spec.rb
|
314
319
|
- spec/disallow_password_reuse_spec.rb
|
320
|
+
- spec/http_basic_auth_spec.rb
|
315
321
|
- spec/jwt_spec.rb
|
316
322
|
- spec/lockout_spec.rb
|
317
323
|
- spec/login_spec.rb
|
@@ -398,7 +404,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
398
404
|
version: '0'
|
399
405
|
requirements: []
|
400
406
|
rubyforge_project:
|
401
|
-
rubygems_version: 2.
|
407
|
+
rubygems_version: 2.6.6
|
402
408
|
signing_key:
|
403
409
|
specification_version: 4
|
404
410
|
summary: Authentication and Account Management Framework for Rack Applications
|