rodauth 1.5.0 → 1.6.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.
- 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
         |