rodauth-oauth 0.4.3 → 0.6.1
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.md +49 -6
- data/README.md +10 -5
- data/lib/generators/{roda → rodauth}/oauth/install_generator.rb +0 -0
- data/lib/generators/{roda → rodauth}/oauth/templates/app/models/oauth_application.rb +0 -0
- data/lib/generators/{roda → rodauth}/oauth/templates/app/models/oauth_grant.rb +0 -0
- data/lib/generators/{roda → rodauth}/oauth/templates/app/models/oauth_token.rb +0 -0
- data/lib/generators/{roda → rodauth}/oauth/templates/db/migrate/create_rodauth_oauth.rb +1 -1
- data/lib/generators/{roda → rodauth}/oauth/views_generator.rb +1 -1
- data/lib/rodauth/features/oauth.rb +29 -17
- data/lib/rodauth/features/oauth_http_mac.rb +1 -1
- data/lib/rodauth/features/oauth_jwt.rb +111 -32
- data/lib/rodauth/features/oauth_saml.rb +1 -1
- data/lib/rodauth/features/oidc.rb +101 -3
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +9 -9
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: c0c72cd872103e1d10929ad5934312a123a42b3c9cb55c06c118fbcb0d83f4a7
         | 
| 4 | 
            +
              data.tar.gz: 57bbcef2981c20627cfc9239b30781af03898e34b5d2861b84a820d778e1dac3
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: a4c48e1ce93074c5dff85f506c8c5b8c7f024409f9c58ae942bb2adb5241303586cb0930a1c849566c793d6d9e508a73b0f5fc5772a82e3c87852415997e7889
         | 
| 7 | 
            +
              data.tar.gz: 54e5777b2506ea99f830cd3d9b66ca1755372681cd19013638bc25680b0ce601275fb5fd732b123fcefddb7f56ba0ac1fc6a069f028c377be5a7408d92debb9e
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -2,40 +2,83 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            ## master
         | 
| 4 4 |  | 
| 5 | 
            +
            ### 0.6.1 (08/09/2021)
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            #### Bugfixes
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            * Fixed rails view templates escaping.
         | 
| 10 | 
            +
            * Fixed declaration of authorize template in the generator.
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            ### 0.6.0 (21/05/2021)
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            ### Improvements
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            * RBS signatures
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            ### Chore
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            * Ruby 3 and Truffleruby are now officially supported and tested in CI.
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            ### 0.5.1 (19/03/2021)
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            #### Improvements
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            * Changing "Callback URL" to "Redirect URL" in default templates;
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            #### Bugfixes
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            * (rails integration) Fixed templates location;
         | 
| 31 | 
            +
            * (rails integration) Fixed migration name from generator;
         | 
| 32 | 
            +
            * (rails integration) fixed links, html tags, styling and unassigned variables from a few view templates;
         | 
| 33 | 
            +
            * `oauth_application_path` is now compliant with prefixes and other url helpers, while now having a `oauth_application_url` counterpart;
         | 
| 34 | 
            +
            * (rails integration) skipping csrf checks for "/userinfo" request (OIDC)
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            ### 0.5.0 (08/02/2021)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            #### RP-Initiated Logout
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            The `:oidc` plugin can now do [RP-Initiated Logout](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/RP-Initiated-Logout). It's disabled by default, so read the docs to learn how to enable it.
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            #### Security
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            The `:oauth_jwt` (and by association, `:oidc`) plugin(s) verifies the claims of used JWT tokens. This is a **very important security fix**, as without it, there is no protection against replay attacks and other types of misuse of the JWT token.
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            A new auth method, `generate_jti(claims)`, was [added to the list of oauth_jwt plugin options](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/JWT-Access-Tokens#rodauth-options). By default, it'll hash the `aud` and `iat` claims together, but you can overwrite how this is done.
         | 
| 47 | 
            +
             | 
| 5 48 | 
             
            ### 0.4.3 (09/12/2020)
         | 
| 6 49 |  | 
| 7 50 | 
             
            * Introspection requests made to an Authorization Server in "resource server" mode are not correctly encoding the body using the "application/x-www-form-urlencoded" format.
         | 
| 8 51 |  | 
| 9 52 | 
             
            ### 0.4.2 (24/11/2020)
         | 
| 10 53 |  | 
| 11 | 
            -
             | 
| 54 | 
            +
            #### Bugfixes
         | 
| 12 55 |  | 
| 13 56 | 
             
            * database extensions were being run in resource server mode, when it's not expected that the oauth db tables are around.
         | 
| 14 57 |  | 
| 15 58 | 
             
            ### 0.4.1 (24/11/2020)
         | 
| 16 59 |  | 
| 17 | 
            -
             | 
| 60 | 
            +
            #### Improvements
         | 
| 18 61 |  | 
| 19 62 | 
             
            When in "Resource Server" mode, calling `rodauth.authorization_token` will now return an hash of the JSON payload that the Authorization Server responds, and which was already previously used to authorize access to protected resources.
         | 
| 20 63 |  | 
| 21 | 
            -
             | 
| 64 | 
            +
            #### Bugfixes
         | 
| 22 65 |  | 
| 23 66 | 
             
            * An error occurred if the client passed an empty authorization header (`Authorization: ` or `Authorization: Bearer `), causing an unexpected error; It now responds with the proper `401 Unauthorized` status code.
         | 
| 24 67 |  | 
| 25 68 | 
             
            ### 0.4.0 (13/11/2020)
         | 
| 26 69 |  | 
| 27 | 
            -
             | 
| 70 | 
            +
            #### Features
         | 
| 28 71 |  | 
| 29 72 | 
             
            * A new method, `get_additional_param(account, claim)`, is now exposed; this method will be called whenever non-OIDC scopes are requested in the emission of the ID token.
         | 
| 30 73 |  | 
| 31 74 | 
             
            * The `form_post` response is now supported, either by passing the `response_mode=form_post` request param in the authorization URL, or by setting `oauth_response_mode "form_post"` option. This improves the overall security of an Authorization server even more, as authorization codes are sent to client applications via a POST request to the redirect URI.
         | 
| 32 75 |  | 
| 33 76 |  | 
| 34 | 
            -
             | 
| 77 | 
            +
            #### Improvements
         | 
| 35 78 |  | 
| 36 79 | 
             
            * For the OIDC `address` scope, proper claims are now emitted as per the standard, i.e. the "formatted", "street_address", "locality", "region", "postal_code", "country". These will be the ones referenced in the `get_oidc_param` method.
         | 
| 37 80 |  | 
| 38 | 
            -
             | 
| 81 | 
            +
            #### Bugfixes
         | 
| 39 82 |  | 
| 40 83 | 
             
            * The rails templates were missing declarations from a few params, which made some of the flows (the PKCE for example) not work out-of-the box;
         | 
| 41 84 | 
             
            * rails tests were silently not running in CI;
         | 
    
        data/README.md
    CHANGED
    
    | @@ -25,7 +25,12 @@ This gem implements the following RFCs and features of OAuth: | |
| 25 25 | 
             
            * [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
         | 
| 26 26 | 
             
            * OAuth application and token management dashboards;
         | 
| 27 27 |  | 
| 28 | 
            -
            It also implements the [OpenID Connect layer](https://openid.net/connect/) on top of the OAuth features it provides | 
| 28 | 
            +
            It also implements the [OpenID Connect layer](https://openid.net/connect/) on top of the OAuth features it provides, including:
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html);
         | 
| 31 | 
            +
            * [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0-29.html);
         | 
| 32 | 
            +
            * [OpenID Multiple Response Types](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html);
         | 
| 33 | 
            +
            * [RP Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html);
         | 
| 29 34 |  | 
| 30 35 | 
             
            This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
         | 
| 31 36 |  | 
| @@ -104,7 +109,7 @@ For OpenID, it's very similar to the example above: | |
| 104 109 | 
             
            ```ruby
         | 
| 105 110 | 
             
            plugin :rodauth do
         | 
| 106 111 | 
             
              # enable it in the plugin
         | 
| 107 | 
            -
              enable :login, : | 
| 112 | 
            +
              enable :login, :oidc
         | 
| 108 113 | 
             
              oauth_application_default_scope %w[openid]
         | 
| 109 114 | 
             
              oauth_application_scopes %w[openid email profile]
         | 
| 110 115 | 
             
            end
         | 
| @@ -628,11 +633,11 @@ Although very handy for the mentioned use case, one can't revoke a JWT token on | |
| 628 633 |  | 
| 629 634 | 
             
            ## Ruby support policy
         | 
| 630 635 |  | 
| 631 | 
            -
            The minimum Ruby version required to run `rodauth-oauth` is 2.3 . Besides that, it should support all rubies that rodauth and roda support, including JRuby and  | 
| 636 | 
            +
            The minimum Ruby version required to run `rodauth-oauth` is 2.3 . Besides that, it should support all rubies that rodauth and roda support, including JRuby and truffleruby.
         | 
| 632 637 |  | 
| 633 | 
            -
            ###  | 
| 638 | 
            +
            ### Rails
         | 
| 634 639 |  | 
| 635 | 
            -
            If you're interested in using this library  | 
| 640 | 
            +
            If you're interested in using this library with rails, be sure to check `rodauth-rails` policy, as it supports rails 5.2 upwards.
         | 
| 636 641 |  | 
| 637 642 | 
             
            ## Development
         | 
| 638 643 |  | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| 
            File without changes
         | 
| @@ -9,7 +9,7 @@ module Rodauth::OAuth | |
| 9 9 | 
             
                    source_root "#{__dir__}/templates"
         | 
| 10 10 | 
             
                    namespace "rodauth:oauth:views"
         | 
| 11 11 |  | 
| 12 | 
            -
                    DEFAULT = %w[ | 
| 12 | 
            +
                    DEFAULT = %w[authorize].freeze
         | 
| 13 13 | 
             
                    VIEWS = {
         | 
| 14 14 | 
             
                      oauth_authorize: DEFAULT,
         | 
| 15 15 | 
             
                      oauth_applications: %w[oauth_applications oauth_application new_oauth_application]
         | 
| @@ -9,7 +9,7 @@ require "rodauth/oauth/ttl_store" | |
| 9 9 | 
             
            require "rodauth/oauth/database_extensions"
         | 
| 10 10 |  | 
| 11 11 | 
             
            module Rodauth
         | 
| 12 | 
            -
              Feature.define(:oauth) do
         | 
| 12 | 
            +
              Feature.define(:oauth, :Oauth) do
         | 
| 13 13 | 
             
                # RUBY EXTENSIONS
         | 
| 14 14 | 
             
                unless Regexp.method_defined?(:match?)
         | 
| 15 15 | 
             
                  # If you wonder why this is there: the oauth feature uses a refinement to enhance the
         | 
| @@ -139,7 +139,15 @@ module Rodauth | |
| 139 139 | 
             
                auth_value_method :already_in_use_response_status, 409
         | 
| 140 140 |  | 
| 141 141 | 
             
                # OAuth Applications
         | 
| 142 | 
            -
                auth_value_method : | 
| 142 | 
            +
                auth_value_method :oauth_applications_route, "oauth-applications"
         | 
| 143 | 
            +
                def oauth_applications_path(opts = {})
         | 
| 144 | 
            +
                  route_path(oauth_applications_route, opts)
         | 
| 145 | 
            +
                end
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                def oauth_applications_url(opts = {})
         | 
| 148 | 
            +
                  route_url(oauth_applications_route, opts)
         | 
| 149 | 
            +
                end
         | 
| 150 | 
            +
             | 
| 143 151 | 
             
                auth_value_method :oauth_applications_table, :oauth_applications
         | 
| 144 152 |  | 
| 145 153 | 
             
                auth_value_method :oauth_applications_id_column, :id
         | 
| @@ -192,6 +200,7 @@ module Rodauth | |
| 192 200 | 
             
                auth_value_method :oauth_unique_id_generation_retries, 3
         | 
| 193 201 |  | 
| 194 202 | 
             
                auth_value_methods(
         | 
| 203 | 
            +
                  :oauth_application_path,
         | 
| 195 204 | 
             
                  :fetch_access_token,
         | 
| 196 205 | 
             
                  :oauth_unique_id_generator,
         | 
| 197 206 | 
             
                  :secret_matches?,
         | 
| @@ -363,9 +372,13 @@ module Rodauth | |
| 363 372 | 
             
                  end
         | 
| 364 373 | 
             
                end
         | 
| 365 374 |  | 
| 375 | 
            +
                def oauth_application_path(id)
         | 
| 376 | 
            +
                  "#{oauth_applications_path}/#{id}"
         | 
| 377 | 
            +
                end
         | 
| 378 | 
            +
             | 
| 366 379 | 
             
                # /oauth-applications routes
         | 
| 367 380 | 
             
                def oauth_applications
         | 
| 368 | 
            -
                  request.on( | 
| 381 | 
            +
                  request.on(oauth_applications_route) do
         | 
| 369 382 | 
             
                    require_account
         | 
| 370 383 |  | 
| 371 384 | 
             
                    request.get "new" do
         | 
| @@ -422,16 +435,20 @@ module Rodauth | |
| 422 435 | 
             
                    false
         | 
| 423 436 | 
             
                  when revoke_path
         | 
| 424 437 | 
             
                    !json_request?
         | 
| 425 | 
            -
                  when authorize_path,  | 
| 438 | 
            +
                  when authorize_path, oauth_applications_path
         | 
| 426 439 | 
             
                    only_json? ? false : super
         | 
| 427 440 | 
             
                  else
         | 
| 428 441 | 
             
                    super
         | 
| 429 442 | 
             
                  end
         | 
| 430 443 | 
             
                end
         | 
| 431 444 |  | 
| 432 | 
            -
                # Overrides  | 
| 433 | 
            -
                def  | 
| 434 | 
            -
                  super ||  | 
| 445 | 
            +
                # Overrides session_value, so that a valid authorization token also authenticates a request
         | 
| 446 | 
            +
                def session_value
         | 
| 447 | 
            +
                  super || begin
         | 
| 448 | 
            +
                    return unless authorization_token
         | 
| 449 | 
            +
             | 
| 450 | 
            +
                    authorization_token[oauth_tokens_account_id_column]
         | 
| 451 | 
            +
                  end
         | 
| 435 452 | 
             
                end
         | 
| 436 453 |  | 
| 437 454 | 
             
                def accepts_json?
         | 
| @@ -449,10 +466,6 @@ module Rodauth | |
| 449 466 | 
             
                  end
         | 
| 450 467 | 
             
                end
         | 
| 451 468 |  | 
| 452 | 
            -
                def initialize(scope)
         | 
| 453 | 
            -
                  @scope = scope
         | 
| 454 | 
            -
                end
         | 
| 455 | 
            -
             | 
| 456 469 | 
             
                def scopes
         | 
| 457 470 | 
             
                  scope = request.params["scope"]
         | 
| 458 471 | 
             
                  case scope
         | 
| @@ -551,12 +564,11 @@ module Rodauth | |
| 551 564 | 
             
                  self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
         | 
| 552 565 |  | 
| 553 566 | 
             
                  # Check whether we can reutilize db entries for the same account / application pair
         | 
| 554 | 
            -
                  one_oauth_token_per_account =  | 
| 555 | 
            -
                     | 
| 556 | 
            -
                      definition[: | 
| 557 | 
            -
                        definition[:columns] == oauth_tokens_unique_columns
         | 
| 558 | 
            -
                    end
         | 
| 567 | 
            +
                  one_oauth_token_per_account = db.indexes(oauth_tokens_table).values.any? do |definition|
         | 
| 568 | 
            +
                    definition[:unique] &&
         | 
| 569 | 
            +
                      definition[:columns] == oauth_tokens_unique_columns
         | 
| 559 570 | 
             
                  end
         | 
| 571 | 
            +
             | 
| 560 572 | 
             
                  self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
         | 
| 561 573 | 
             
                end
         | 
| 562 574 |  | 
| @@ -1350,7 +1362,7 @@ module Rodauth | |
| 1350 1362 | 
             
                    issuer: issuer,
         | 
| 1351 1363 | 
             
                    authorization_endpoint: authorize_url,
         | 
| 1352 1364 | 
             
                    token_endpoint: token_url,
         | 
| 1353 | 
            -
                    registration_endpoint:  | 
| 1365 | 
            +
                    registration_endpoint: oauth_applications_url,
         | 
| 1354 1366 | 
             
                    scopes_supported: oauth_application_scopes,
         | 
| 1355 1367 | 
             
                    response_types_supported: responses_supported,
         | 
| 1356 1368 | 
             
                    response_modes_supported: response_modes_supported,
         | 
| @@ -3,11 +3,13 @@ | |
| 3 3 | 
             
            require "rodauth/oauth/ttl_store"
         | 
| 4 4 |  | 
| 5 5 | 
             
            module Rodauth
         | 
| 6 | 
            -
              Feature.define(:oauth_jwt) do
         | 
| 6 | 
            +
              Feature.define(:oauth_jwt, :OauthJwt) do
         | 
| 7 7 | 
             
                depends :oauth
         | 
| 8 8 |  | 
| 9 9 | 
             
                JWKS = OAuth::TtlStore.new
         | 
| 10 10 |  | 
| 11 | 
            +
                # Recommended to have hmac_secret as well
         | 
| 12 | 
            +
             | 
| 11 13 | 
             
                auth_value_method :oauth_jwt_subject_type, "public" # public, pairwise
         | 
| 12 14 | 
             
                auth_value_method :oauth_jwt_subject_secret, nil # salt for pairwise generation
         | 
| 13 15 |  | 
| @@ -38,7 +40,8 @@ module Rodauth | |
| 38 40 | 
             
                  :jwt_encode,
         | 
| 39 41 | 
             
                  :jwt_decode,
         | 
| 40 42 | 
             
                  :jwks_set,
         | 
| 41 | 
            -
                  :last_account_login_at
         | 
| 43 | 
            +
                  :last_account_login_at,
         | 
| 44 | 
            +
                  :generate_jti
         | 
| 42 45 | 
             
                )
         | 
| 43 46 |  | 
| 44 47 | 
             
                route(:jwks) do |r|
         | 
| @@ -59,6 +62,15 @@ module Rodauth | |
| 59 62 | 
             
                  authorization_required unless scopes.any? { |scope| token_scopes.include?(scope) }
         | 
| 60 63 | 
             
                end
         | 
| 61 64 |  | 
| 65 | 
            +
                # Overrides session_value, so that a valid authorization token also authenticates a request
         | 
| 66 | 
            +
                def session_value
         | 
| 67 | 
            +
                  super || begin
         | 
| 68 | 
            +
                    return unless authorization_token
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    authorization_token["sub"]
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 62 74 | 
             
                private
         | 
| 63 75 |  | 
| 64 76 | 
             
                unless method_defined?(:last_account_login_at)
         | 
| @@ -67,6 +79,10 @@ module Rodauth | |
| 67 79 | 
             
                  end
         | 
| 68 80 | 
             
                end
         | 
| 69 81 |  | 
| 82 | 
            +
                def issuer
         | 
| 83 | 
            +
                  @issuer ||= oauth_jwt_token_issuer || authorization_server_url
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 70 86 | 
             
                def authorization_token
         | 
| 71 87 | 
             
                  return @authorization_token if defined?(@authorization_token)
         | 
| 72 88 |  | 
| @@ -79,7 +95,7 @@ module Rodauth | |
| 79 95 |  | 
| 80 96 | 
             
                    return unless jwt_token
         | 
| 81 97 |  | 
| 82 | 
            -
                    return if jwt_token["iss"] !=  | 
| 98 | 
            +
                    return if jwt_token["iss"] != issuer ||
         | 
| 83 99 | 
             
                              (oauth_jwt_audience && jwt_token["aud"] != oauth_jwt_audience) ||
         | 
| 84 100 | 
             
                              !jwt_token["sub"]
         | 
| 85 101 |  | 
| @@ -105,7 +121,7 @@ module Rodauth | |
| 105 121 | 
             
                              redirect_response_error("invalid_request_object")
         | 
| 106 122 | 
             
                            end
         | 
| 107 123 |  | 
| 108 | 
            -
                  claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg])
         | 
| 124 | 
            +
                  claims = jwt_decode(request_object, jws_key: jwk_import(jws_jwk), jws_algorithm: jwk[:alg], verify_jti: false)
         | 
| 109 125 |  | 
| 110 126 | 
             
                  redirect_response_error("invalid_request_object") unless claims
         | 
| 111 127 |  | 
| @@ -118,7 +134,7 @@ module Rodauth | |
| 118 134 | 
             
                  claims.delete("iss")
         | 
| 119 135 | 
             
                  audience = claims.delete("aud")
         | 
| 120 136 |  | 
| 121 | 
            -
                  redirect_response_error("invalid_request_object") if audience && audience !=  | 
| 137 | 
            +
                  redirect_response_error("invalid_request_object") if audience && audience != issuer
         | 
| 122 138 |  | 
| 123 139 | 
             
                  claims.each do |k, v|
         | 
| 124 140 | 
             
                    request.params[k.to_s] = v
         | 
| @@ -209,7 +225,7 @@ module Rodauth | |
| 209 225 | 
             
                  issued_at = Time.now.to_i
         | 
| 210 226 |  | 
| 211 227 | 
             
                  claims = {
         | 
| 212 | 
            -
                    iss:  | 
| 228 | 
            +
                    iss: issuer, # issuer
         | 
| 213 229 | 
             
                    iat: issued_at, # issued at
         | 
| 214 230 | 
             
                    #
         | 
| 215 231 | 
             
                    # sub  REQUIRED - as defined in section 4.1.2 of [RFC7519].  In case of
         | 
| @@ -317,6 +333,23 @@ module Rodauth | |
| 317 333 | 
             
                  end
         | 
| 318 334 | 
             
                end
         | 
| 319 335 |  | 
| 336 | 
            +
                def generate_jti(payload)
         | 
| 337 | 
            +
                  # Use the key and iat to create a unique key per request to prevent replay attacks
         | 
| 338 | 
            +
                  jti_raw = [
         | 
| 339 | 
            +
                    payload[:aud] || payload["aud"],
         | 
| 340 | 
            +
                    payload[:iat] || payload["iat"]
         | 
| 341 | 
            +
                  ].join(":").to_s
         | 
| 342 | 
            +
                  Digest::SHA256.hexdigest(jti_raw)
         | 
| 343 | 
            +
                end
         | 
| 344 | 
            +
             | 
| 345 | 
            +
                def verify_jti(jti, claims)
         | 
| 346 | 
            +
                  generate_jti(claims) == jti
         | 
| 347 | 
            +
                end
         | 
| 348 | 
            +
             | 
| 349 | 
            +
                def verify_aud(aud, claims)
         | 
| 350 | 
            +
                  aud == (oauth_jwt_audience || claims["client_id"])
         | 
| 351 | 
            +
                end
         | 
| 352 | 
            +
             | 
| 320 353 | 
             
                if defined?(JSON::JWT)
         | 
| 321 354 |  | 
| 322 355 | 
             
                  def jwk_import(data)
         | 
| @@ -325,6 +358,7 @@ module Rodauth | |
| 325 358 |  | 
| 326 359 | 
             
                  # json-jwt
         | 
| 327 360 | 
             
                  def jwt_encode(payload)
         | 
| 361 | 
            +
                    payload[:jti] = generate_jti(payload)
         | 
| 328 362 | 
             
                    jwt = JSON::JWT.new(payload)
         | 
| 329 363 | 
             
                    jwk = JSON::JWK.new(_jwt_key)
         | 
| 330 364 |  | 
| @@ -340,18 +374,34 @@ module Rodauth | |
| 340 374 | 
             
                    jwt.to_s
         | 
| 341 375 | 
             
                  end
         | 
| 342 376 |  | 
| 343 | 
            -
                  def jwt_decode( | 
| 377 | 
            +
                  def jwt_decode(
         | 
| 378 | 
            +
                    token,
         | 
| 379 | 
            +
                    jws_key: oauth_jwt_public_key || _jwt_key,
         | 
| 380 | 
            +
                    verify_claims: true,
         | 
| 381 | 
            +
                    verify_jti: true,
         | 
| 382 | 
            +
                    **
         | 
| 383 | 
            +
                  )
         | 
| 344 384 | 
             
                    token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
         | 
| 345 385 |  | 
| 346 | 
            -
                    if is_authorization_server?
         | 
| 347 | 
            -
             | 
| 348 | 
            -
             | 
| 349 | 
            -
             | 
| 350 | 
            -
             | 
| 351 | 
            -
             | 
| 352 | 
            -
             | 
| 353 | 
            -
             | 
| 386 | 
            +
                    claims = if is_authorization_server?
         | 
| 387 | 
            +
                               if oauth_jwt_legacy_public_key
         | 
| 388 | 
            +
                                 JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
         | 
| 389 | 
            +
                               elsif jws_key
         | 
| 390 | 
            +
                                 JSON::JWT.decode(token, jws_key)
         | 
| 391 | 
            +
                               end
         | 
| 392 | 
            +
                             elsif (jwks = auth_server_jwks_set)
         | 
| 393 | 
            +
                               JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
         | 
| 394 | 
            +
                             end
         | 
| 395 | 
            +
             | 
| 396 | 
            +
                    if verify_claims && !(claims[:iss] == issuer &&
         | 
| 397 | 
            +
                        verify_aud(claims[:aud], claims) &&
         | 
| 398 | 
            +
                        (!claims[:iat] || Time.at(claims[:iat]) > (Time.now - oauth_token_expires_in)) &&
         | 
| 399 | 
            +
                        (!claims[:exp] || Time.at(claims[:exp]) > Time.now) &&
         | 
| 400 | 
            +
                        (!verify_jti || verify_jti(claims[:jti], claims)))
         | 
| 401 | 
            +
                      return
         | 
| 354 402 | 
             
                    end
         | 
| 403 | 
            +
             | 
| 404 | 
            +
                    claims
         | 
| 355 405 | 
             
                  rescue JSON::JWT::Exception
         | 
| 356 406 | 
             
                    nil
         | 
| 357 407 | 
             
                  end
         | 
| @@ -384,12 +434,8 @@ module Rodauth | |
| 384 434 | 
             
                      key = jwk.keypair
         | 
| 385 435 | 
             
                    end
         | 
| 386 436 |  | 
| 387 | 
            -
                    # Use the key and iat to create a unique key per request to prevent replay attacks
         | 
| 388 | 
            -
                    jti_raw = [key, payload[:iat]].join(":").to_s
         | 
| 389 | 
            -
                    jti = Digest::SHA256.hexdigest(jti_raw)
         | 
| 390 | 
            -
             | 
| 391 437 | 
             
                    # @see JWT reserved claims - https://tools.ietf.org/html/draft-jones-json-web-token-07#page-7
         | 
| 392 | 
            -
                    payload[:jti] =  | 
| 438 | 
            +
                    payload[:jti] = generate_jti(payload)
         | 
| 393 439 | 
             
                    token = JWT.encode(payload, key, oauth_jwt_algorithm, headers)
         | 
| 394 440 |  | 
| 395 441 | 
             
                    if oauth_jwt_jwe_key
         | 
| @@ -405,21 +451,54 @@ module Rodauth | |
| 405 451 | 
             
                    token
         | 
| 406 452 | 
             
                  end
         | 
| 407 453 |  | 
| 408 | 
            -
                  def jwt_decode( | 
| 454 | 
            +
                  def jwt_decode(
         | 
| 455 | 
            +
                    token,
         | 
| 456 | 
            +
                    jws_key: oauth_jwt_public_key || _jwt_key,
         | 
| 457 | 
            +
                    jws_algorithm: oauth_jwt_algorithm,
         | 
| 458 | 
            +
                    verify_claims: true,
         | 
| 459 | 
            +
                    verify_jti: true
         | 
| 460 | 
            +
                  )
         | 
| 409 461 | 
             
                    # decrypt jwe
         | 
| 410 462 | 
             
                    token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
         | 
| 463 | 
            +
             | 
| 464 | 
            +
                    # verifying the JWT implies verifying:
         | 
| 465 | 
            +
                    #
         | 
| 466 | 
            +
                    # issuer: check that server generated the token
         | 
| 467 | 
            +
                    # aud: check the audience field (client is who he says he is)
         | 
| 468 | 
            +
                    # iat: check that the token didn't expire
         | 
| 469 | 
            +
                    #
         | 
| 470 | 
            +
                    # subject can't be verified automatically without having access to the account id,
         | 
| 471 | 
            +
                    # which we don't because that's the whole point.
         | 
| 472 | 
            +
                    #
         | 
| 473 | 
            +
                    verify_claims_params = if verify_claims
         | 
| 474 | 
            +
                                             {
         | 
| 475 | 
            +
                                               verify_iss: true,
         | 
| 476 | 
            +
                                               iss: issuer,
         | 
| 477 | 
            +
                                               # can't use stock aud verification, as it's dependent on the client application id
         | 
| 478 | 
            +
                                               verify_aud: false,
         | 
| 479 | 
            +
                                               verify_jti: (verify_jti ? method(:verify_jti) : false),
         | 
| 480 | 
            +
                                               verify_iat: true
         | 
| 481 | 
            +
                                             }
         | 
| 482 | 
            +
                                           else
         | 
| 483 | 
            +
                                             {}
         | 
| 484 | 
            +
                                           end
         | 
| 485 | 
            +
             | 
| 411 486 | 
             
                    # decode jwt
         | 
| 412 | 
            -
                    if is_authorization_server?
         | 
| 413 | 
            -
             | 
| 414 | 
            -
             | 
| 415 | 
            -
             | 
| 416 | 
            -
             | 
| 417 | 
            -
             | 
| 418 | 
            -
             | 
| 419 | 
            -
             | 
| 420 | 
            -
             | 
| 421 | 
            -
             | 
| 422 | 
            -
             | 
| 487 | 
            +
                    claims = if is_authorization_server?
         | 
| 488 | 
            +
                               if oauth_jwt_legacy_public_key
         | 
| 489 | 
            +
                                 algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
         | 
| 490 | 
            +
                                 JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms, **verify_claims_params).first
         | 
| 491 | 
            +
                               elsif jws_key
         | 
| 492 | 
            +
                                 JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
         | 
| 493 | 
            +
                               end
         | 
| 494 | 
            +
                             elsif (jwks = auth_server_jwks_set)
         | 
| 495 | 
            +
                               algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
         | 
| 496 | 
            +
                               JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms, **verify_claims_params).first
         | 
| 497 | 
            +
                             end
         | 
| 498 | 
            +
             | 
| 499 | 
            +
                    return if verify_claims && !verify_aud(claims["aud"], claims)
         | 
| 500 | 
            +
             | 
| 501 | 
            +
                    claims
         | 
| 423 502 | 
             
                  rescue JWT::DecodeError, JWT::JWKError
         | 
| 424 503 | 
             
                    nil
         | 
| 425 504 | 
             
                  end
         | 
| @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            # frozen-string-literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module Rodauth
         | 
| 4 | 
            -
              Feature.define(:oidc) do
         | 
| 4 | 
            +
              Feature.define(:oidc, :Oidc) do
         | 
| 5 5 | 
             
                # https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
         | 
| 6 6 | 
             
                OIDC_SCOPES_MAP = {
         | 
| 7 7 | 
             
                  "profile" => %i[name family_name given_name middle_name nickname preferred_username
         | 
| @@ -14,6 +14,7 @@ module Rodauth | |
| 14 14 | 
             
                VALID_METADATA_KEYS = %i[
         | 
| 15 15 | 
             
                  issuer
         | 
| 16 16 | 
             
                  authorization_endpoint
         | 
| 17 | 
            +
                  end_session_endpoint
         | 
| 17 18 | 
             
                  token_endpoint
         | 
| 18 19 | 
             
                  userinfo_endpoint
         | 
| 19 20 | 
             
                  jwks_uri
         | 
| @@ -75,6 +76,10 @@ module Rodauth | |
| 75 76 | 
             
                auth_value_method :oauth_prompt_login_cookie_options, {}.freeze
         | 
| 76 77 | 
             
                auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
         | 
| 77 78 |  | 
| 79 | 
            +
                # logout
         | 
| 80 | 
            +
                auth_value_method :oauth_applications_post_logout_redirect_uri_column, :post_logout_redirect_uri
         | 
| 81 | 
            +
                auth_value_method :use_rp_initiated_logout?, false
         | 
| 82 | 
            +
             | 
| 78 83 | 
             
                auth_value_methods(:get_oidc_param, :get_additional_param)
         | 
| 79 84 |  | 
| 80 85 | 
             
                # /userinfo
         | 
| @@ -108,10 +113,81 @@ module Rodauth | |
| 108 113 | 
             
                  end
         | 
| 109 114 | 
             
                end
         | 
| 110 115 |  | 
| 111 | 
            -
                 | 
| 116 | 
            +
                # /oidc-logout
         | 
| 117 | 
            +
                route(:oidc_logout) do |r|
         | 
| 118 | 
            +
                  next unless use_rp_initiated_logout?
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  before_oidc_logout_route
         | 
| 121 | 
            +
                  require_authorizable_account
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                  # OpenID Providers MUST support the use of the HTTP GET and POST methods
         | 
| 124 | 
            +
                  r.on method: %i[get post] do
         | 
| 125 | 
            +
                    catch_error do
         | 
| 126 | 
            +
                      validate_oidc_logout_params
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                      #
         | 
| 129 | 
            +
                      # why this is done:
         | 
| 130 | 
            +
                      #
         | 
| 131 | 
            +
                      # we need to decode the id token in order to get the application, because, if the
         | 
| 132 | 
            +
                      # signing key is application-specific, we don't know how to verify the signature
         | 
| 133 | 
            +
                      # beforehand. Hence, we have to do it twice: decode-and-do-not-verify, initialize
         | 
| 134 | 
            +
                      # the @oauth_application, and then decode-and-verify.
         | 
| 135 | 
            +
                      #
         | 
| 136 | 
            +
                      oauth_token = jwt_decode(param("id_token_hint"), verify_claims: false)
         | 
| 137 | 
            +
                      oauth_application_id = oauth_token["client_id"]
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                      # check whether ID token belongs to currently logged-in user
         | 
| 140 | 
            +
                      redirect_response_error("invalid_request") unless oauth_token["sub"] == jwt_subject(
         | 
| 141 | 
            +
                        oauth_tokens_account_id_column => account_id,
         | 
| 142 | 
            +
                        oauth_tokens_oauth_application_id_column => oauth_application_id
         | 
| 143 | 
            +
                      )
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                      # When an id_token_hint parameter is present, the OP MUST validate that it was the issuer of the ID Token.
         | 
| 146 | 
            +
                      redirect_response_error("invalid_request") unless oauth_token && oauth_token["iss"] == issuer
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                      # now let's logout from IdP
         | 
| 149 | 
            +
                      transaction do
         | 
| 150 | 
            +
                        before_logout
         | 
| 151 | 
            +
                        logout
         | 
| 152 | 
            +
                        after_logout
         | 
| 153 | 
            +
                      end
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                      if (post_logout_redirect_uri = param_or_nil("post_logout_redirect_uri"))
         | 
| 156 | 
            +
                        catch(:default_logout_redirect) do
         | 
| 157 | 
            +
                          oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => oauth_token["client_id"]).first
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                          throw(:default_logout_redirect) unless oauth_application
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                          post_logout_redirect_uris = oauth_application[oauth_applications_post_logout_redirect_uri_column].split(" ")
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                          throw(:default_logout_redirect) unless post_logout_redirect_uris.include?(post_logout_redirect_uri)
         | 
| 164 | 
            +
             | 
| 165 | 
            +
                          if (state = param_or_nil("state"))
         | 
| 166 | 
            +
                            post_logout_redirect_uri = URI(post_logout_redirect_uri)
         | 
| 167 | 
            +
                            params = ["state=#{state}"]
         | 
| 168 | 
            +
                            params << post_logout_redirect_uri.query if post_logout_redirect_uri.query
         | 
| 169 | 
            +
                            post_logout_redirect_uri.query = params.join("&")
         | 
| 170 | 
            +
                            post_logout_redirect_uri = post_logout_redirect_uri.to_s
         | 
| 171 | 
            +
                          end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                          redirect(post_logout_redirect_uri)
         | 
| 174 | 
            +
                        end
         | 
| 175 | 
            +
             | 
| 176 | 
            +
                      end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                      # regular logout procedure
         | 
| 179 | 
            +
                      set_notice_flash(logout_notice_flash)
         | 
| 180 | 
            +
                      redirect(logout_redirect)
         | 
| 181 | 
            +
                    end
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                    redirect_response_error("invalid_request")
         | 
| 184 | 
            +
                  end
         | 
| 185 | 
            +
                end
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                def openid_configuration(alt_issuer = nil)
         | 
| 112 188 | 
             
                  request.on(".well-known/openid-configuration") do
         | 
| 113 189 | 
             
                    request.get do
         | 
| 114 | 
            -
                      json_response_success(openid_configuration_body( | 
| 190 | 
            +
                      json_response_success(openid_configuration_body(alt_issuer), cache: true)
         | 
| 115 191 | 
             
                    end
         | 
| 116 192 | 
             
                  end
         | 
| 117 193 | 
             
                end
         | 
| @@ -139,6 +215,15 @@ module Rodauth | |
| 139 215 | 
             
                  end
         | 
| 140 216 | 
             
                end
         | 
| 141 217 |  | 
| 218 | 
            +
                def check_csrf?
         | 
| 219 | 
            +
                  case request.path
         | 
| 220 | 
            +
                  when userinfo_path
         | 
| 221 | 
            +
                    false
         | 
| 222 | 
            +
                  else
         | 
| 223 | 
            +
                    super
         | 
| 224 | 
            +
                  end
         | 
| 225 | 
            +
                end
         | 
| 226 | 
            +
             | 
| 142 227 | 
             
                private
         | 
| 143 228 |  | 
| 144 229 | 
             
                def require_authorizable_account
         | 
| @@ -342,6 +427,18 @@ module Rodauth | |
| 342 427 | 
             
                  params
         | 
| 343 428 | 
             
                end
         | 
| 344 429 |  | 
| 430 | 
            +
                # Logout
         | 
| 431 | 
            +
             | 
| 432 | 
            +
                def validate_oidc_logout_params
         | 
| 433 | 
            +
                  redirect_response_error("invalid_request") unless param_or_nil("id_token_hint")
         | 
| 434 | 
            +
                  # check if valid token hint type
         | 
| 435 | 
            +
                  return unless (redirect_uri = param_or_nil("post_logout_redirect_uri"))
         | 
| 436 | 
            +
             | 
| 437 | 
            +
                  return if check_valid_uri?(redirect_uri)
         | 
| 438 | 
            +
             | 
| 439 | 
            +
                  redirect_response_error("invalid_request")
         | 
| 440 | 
            +
                end
         | 
| 441 | 
            +
             | 
| 345 442 | 
             
                # Metadata
         | 
| 346 443 |  | 
| 347 444 | 
             
                def openid_configuration_body(path)
         | 
| @@ -368,6 +465,7 @@ module Rodauth | |
| 368 465 |  | 
| 369 466 | 
             
                  metadata.merge(
         | 
| 370 467 | 
             
                    userinfo_endpoint: userinfo_url,
         | 
| 468 | 
            +
                    end_session_endpoint: (oidc_logout_url if use_rp_initiated_logout?),
         | 
| 371 469 | 
             
                    response_types_supported: response_types_supported,
         | 
| 372 470 | 
             
                    subject_types_supported: [oauth_jwt_subject_type],
         | 
| 373 471 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: rodauth-oauth
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.6.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Tiago Cardoso
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2021-09-08 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies: []
         | 
| 13 13 | 
             
            description: Implementation of the OAuth 2.0 protocol on top of rodauth.
         | 
| 14 14 | 
             
            email:
         | 
| @@ -23,12 +23,12 @@ files: | |
| 23 23 | 
             
            - CHANGELOG.md
         | 
| 24 24 | 
             
            - LICENSE.txt
         | 
| 25 25 | 
             
            - README.md
         | 
| 26 | 
            -
            - lib/generators/ | 
| 27 | 
            -
            - lib/generators/ | 
| 28 | 
            -
            - lib/generators/ | 
| 29 | 
            -
            - lib/generators/ | 
| 30 | 
            -
            - lib/generators/ | 
| 31 | 
            -
            - lib/generators/ | 
| 26 | 
            +
            - lib/generators/rodauth/oauth/install_generator.rb
         | 
| 27 | 
            +
            - lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
         | 
| 28 | 
            +
            - lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
         | 
| 29 | 
            +
            - lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb
         | 
| 30 | 
            +
            - lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb
         | 
| 31 | 
            +
            - lib/generators/rodauth/oauth/views_generator.rb
         | 
| 32 32 | 
             
            - lib/rodauth/features/oauth.rb
         | 
| 33 33 | 
             
            - lib/rodauth/features/oauth_http_mac.rb
         | 
| 34 34 | 
             
            - lib/rodauth/features/oauth_jwt.rb
         | 
| @@ -71,7 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 71 71 | 
             
                - !ruby/object:Gem::Version
         | 
| 72 72 | 
             
                  version: '0'
         | 
| 73 73 | 
             
            requirements: []
         | 
| 74 | 
            -
            rubygems_version: 3. | 
| 74 | 
            +
            rubygems_version: 3.2.15
         | 
| 75 75 | 
             
            signing_key:
         | 
| 76 76 | 
             
            specification_version: 4
         | 
| 77 77 | 
             
            summary: Implementation of the OAuth 2.0 protocol on top of rodauth.
         |