rodauth-oauth 0.1.0 → 0.2.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.md +44 -0
- data/README.md +1 -0
- data/lib/generators/roda/oauth/templates/db/migrate/create_rodauth_oauth.rb +4 -4
- data/lib/rodauth/features/oauth.rb +148 -142
- data/lib/rodauth/features/oauth_http_mac.rb +0 -2
- data/lib/rodauth/features/oauth_jwt.rb +56 -38
- data/lib/rodauth/features/oauth_saml.rb +104 -0
- data/lib/rodauth/oauth/database_extensions.rb +73 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- metadata +4 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 02d69464053b6809900da774b4c9957d642b003d0a0de7aa076e57a5eb8895bc
         | 
| 4 | 
            +
              data.tar.gz: e1dd94b69aa4bdf051b1c28d684a0ec2a1435da9f99ca6bf77da9d537474f9a6
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: cfe325a2e8daa96a72b4577566ae84772dcf114411ebbd9d0115f0f33f5b104f7c138a1b1ef7813d46f0fd2226c6560db5d974e51ec92b33ce7e393726005b2d
         | 
| 7 | 
            +
              data.tar.gz: df5866c1cd089c0d361a00d1ffd1db02df9b6551940dbf70e7390657fa8558ae0fb3c8eb2fcff6ec6fb9fd52406a99615574305d5a3bacdbd20f5fc22bacd63f
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -2,6 +2,50 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            ## master
         | 
| 4 4 |  | 
| 5 | 
            +
            ### 0.2.0
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            #### Features
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ##### SAML Assertion Grant Type
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            `rodauth-auth` now supports using a SAML Assertion to request for an Access token.In order to enable, you have to:
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            ```ruby
         | 
| 14 | 
            +
            plugin :rodauth do
         | 
| 15 | 
            +
              enable :oauth_saml
         | 
| 16 | 
            +
            end
         | 
| 17 | 
            +
            ```
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            For more info about integrating it, [check the wiki](https://gitlab.com/honeyryderchuck/rodauth-oauth/-/wikis/SAML-Assertion-Access-Tokens).
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            ##### Supporting rotating keys
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            At some point, you'll want to replace the pkeys and algorithm used to generate and verify the JWT access tokens, but you want to keep validating previously-distributed JWT tokens, at least until they expire. Now you can, via two new options, `oauth_jwt_legacy_public_key` and `oauth_jwt_legacy_algorithm`, which will be declared in the JWKs URI and used to verify access tokens.
         | 
| 24 | 
            +
             | 
| 25 | 
            +
             | 
| 26 | 
            +
            ##### Reuse access tokens
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            If the `oauth_reuse_access_token` is set, if there's already an existing valid access token, any new grant for the same application / account / scope will keep the same access token. This can be helpful in scenarios where one wants the same access token distributed across devices.
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            ##### require_authorizable_account
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            The method used to verify access to the authorize flow is called `require_authorizable_account`. By default, it checks if a user is logged in by using rodauth's own `require_account`. This is the method you'd want to redefine in order to augment these requirements, i.e. request 2fa authentication.
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            #### Improvements
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            Expired and revoked access tokens end up generating a lot of garbage, which will have to be periodically cleaned up. You can mitigate this now by setting a uniqueness index for a group of columns, i.e. if you set a uniqueness index for the `oauth_application_id/account_id/scopes` column, `rodauth-oauth` will transparently reuse the same db entry to store the new access token. If setting some other type of uniqueness index, make sure to update the option `oauth_tokens_unique_columns` (the array of columns from the uniqueness index).
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            #### Bugfixes
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            Calling `before_*_route` callbacks appropriately.
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            Fixed some mishandling of HTTP headers when in in resource-server mode.
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            #### Chore
         | 
| 45 | 
            +
             | 
| 46 | 
            +
            * 97.7% test coverage;
         | 
| 47 | 
            +
            * `rodauth-oauth` CI tests run against sqlite, postgresql and mysql.
         | 
| 48 | 
            +
             | 
| 5 49 | 
             
            ### 0.1.0
         | 
| 6 50 |  | 
| 7 51 | 
             
            (31/7/2020)
         | 
    
        data/README.md
    CHANGED
    
    | @@ -21,6 +21,7 @@ This gem implements the following RFCs and features of OAuth: | |
| 21 21 | 
             
            * Access Type (Token refresh online and offline);
         | 
| 22 22 | 
             
            * [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
         | 
| 23 23 | 
             
            * [JWT Acess Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07);
         | 
| 24 | 
            +
            * [SAML 2.0 Assertion Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-saml2-bearer-03);
         | 
| 24 25 | 
             
            * [JWT Secured Authorization Requests](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
         | 
| 25 26 | 
             
            * OAuth application and token management dashboards;
         | 
| 26 27 |  | 
| @@ -43,14 +43,14 @@ class CreateRodauthOAuth < ActiveRecord::Migration<%= migration_version %> | |
| 43 43 | 
             
                  t.foreign_key :oauth_tokens, column: :oauth_token_id
         | 
| 44 44 | 
             
                  t.integer :oauth_application_id
         | 
| 45 45 | 
             
                  t.foreign_key :oauth_applications, column: :oauth_application_id
         | 
| 46 | 
            -
                  t.string :token, null: false, token: true
         | 
| 46 | 
            +
                  t.string :token, null: false, token: true, unique: true
         | 
| 47 47 | 
             
                  # uncomment if setting oauth_tokens_token_hash_column
         | 
| 48 48 | 
             
                  # and delete the token column
         | 
| 49 | 
            -
                  # t.string :token_hash, token: true
         | 
| 50 | 
            -
                  t.string :refresh_token
         | 
| 49 | 
            +
                  # t.string :token_hash, token: true, unique: true
         | 
| 50 | 
            +
                  t.string :refresh_token, unique: true
         | 
| 51 51 | 
             
                  # uncomment if setting oauth_tokens_refresh_token_hash_column
         | 
| 52 52 | 
             
                  # and delete the refresh_token column
         | 
| 53 | 
            -
                  # t.string :refresh_token_hash, token: true
         | 
| 53 | 
            +
                  # t.string :refresh_token_hash, token: true, unique: true
         | 
| 54 54 | 
             
                  t.datetime :expires_in, null: false
         | 
| 55 55 | 
             
                  t.datetime :revoked_at
         | 
| 56 56 | 
             
                  t.string :scopes, null: false
         | 
| @@ -1,16 +1,21 @@ | |
| 1 1 | 
             
            # frozen-string-literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            require "time"
         | 
| 3 4 | 
             
            require "base64"
         | 
| 4 5 | 
             
            require "securerandom"
         | 
| 5 6 | 
             
            require "net/http"
         | 
| 6 7 |  | 
| 7 8 | 
             
            require "rodauth/oauth/ttl_store"
         | 
| 9 | 
            +
            require "rodauth/oauth/database_extensions"
         | 
| 8 10 |  | 
| 9 11 | 
             
            module Rodauth
         | 
| 10 12 | 
             
              Feature.define(:oauth) do
         | 
| 11 13 | 
             
                # RUBY EXTENSIONS
         | 
| 12 | 
            -
                # :nocov:
         | 
| 13 14 | 
             
                unless Regexp.method_defined?(:match?)
         | 
| 15 | 
            +
                  # If you wonder why this is there: the oauth feature uses a refinement to enhance the
         | 
| 16 | 
            +
                  # Regexp class locally with #match? , but this is never tested, because ActiveSupport
         | 
| 17 | 
            +
                  # monkey-patches the same method... Please ActiveSupport, stop being so intrusive!
         | 
| 18 | 
            +
                  # :nocov:
         | 
| 14 19 | 
             
                  module RegexpExtensions
         | 
| 15 20 | 
             
                    refine(Regexp) do
         | 
| 16 21 | 
             
                      def match?(*args)
         | 
| @@ -19,6 +24,7 @@ module Rodauth | |
| 19 24 | 
             
                    end
         | 
| 20 25 | 
             
                  end
         | 
| 21 26 | 
             
                  using(RegexpExtensions)
         | 
| 27 | 
            +
                  # :nocov:
         | 
| 22 28 | 
             
                end
         | 
| 23 29 |  | 
| 24 30 | 
             
                unless String.method_defined?(:delete_suffix!)
         | 
| @@ -37,7 +43,6 @@ module Rodauth | |
| 37 43 | 
             
                  end
         | 
| 38 44 | 
             
                  using(SuffixExtensions)
         | 
| 39 45 | 
             
                end
         | 
| 40 | 
            -
                # :nocov:
         | 
| 41 46 |  | 
| 42 47 | 
             
                SCOPES = %w[profile.read].freeze
         | 
| 43 48 |  | 
| @@ -110,6 +115,8 @@ module Rodauth | |
| 110 115 | 
             
                auth_value_method :oauth_tokens_token_hash_column, nil
         | 
| 111 116 | 
             
                auth_value_method :oauth_tokens_refresh_token_hash_column, nil
         | 
| 112 117 |  | 
| 118 | 
            +
                # Access Token reuse
         | 
| 119 | 
            +
                auth_value_method :oauth_reuse_access_token, false
         | 
| 113 120 | 
             
                # OAuth Grants
         | 
| 114 121 | 
             
                auth_value_method :oauth_grants_table, :oauth_grants
         | 
| 115 122 | 
             
                auth_value_method :oauth_grants_id_column, :id
         | 
| @@ -124,6 +131,7 @@ module Rodauth | |
| 124 131 |  | 
| 125 132 | 
             
                auth_value_method :authorization_required_error_status, 401
         | 
| 126 133 | 
             
                auth_value_method :invalid_oauth_response_status, 400
         | 
| 134 | 
            +
                auth_value_method :already_in_use_response_status, 409
         | 
| 127 135 |  | 
| 128 136 | 
             
                # OAuth Applications
         | 
| 129 137 | 
             
                auth_value_method :oauth_applications_path, "oauth-applications"
         | 
| @@ -155,6 +163,8 @@ module Rodauth | |
| 155 163 |  | 
| 156 164 | 
             
                auth_value_method :unique_error_message, "is already in use"
         | 
| 157 165 | 
             
                auth_value_method :null_error_message, "is not filled"
         | 
| 166 | 
            +
                auth_value_method :already_in_use_message, "error generating unique token"
         | 
| 167 | 
            +
                auth_value_method :already_in_use_error_code, "invalid_request"
         | 
| 158 168 |  | 
| 159 169 | 
             
                # PKCE
         | 
| 160 170 | 
             
                auth_value_method :code_challenge_required_error_code, "invalid_request"
         | 
| @@ -172,6 +182,8 @@ module Rodauth | |
| 172 182 | 
             
                # Only required to use if the plugin is to be used in a resource server
         | 
| 173 183 | 
             
                auth_value_method :is_authorization_server?, true
         | 
| 174 184 |  | 
| 185 | 
            +
                auth_value_method :oauth_unique_id_generation_retries, 3
         | 
| 186 | 
            +
             | 
| 175 187 | 
             
                auth_value_methods(
         | 
| 176 188 | 
             
                  :fetch_access_token,
         | 
| 177 189 | 
             
                  :oauth_unique_id_generator,
         | 
| @@ -179,7 +191,9 @@ module Rodauth | |
| 179 191 | 
             
                  :secret_hash,
         | 
| 180 192 | 
             
                  :generate_token_hash,
         | 
| 181 193 | 
             
                  :authorization_server_url,
         | 
| 182 | 
            -
                  :before_introspection_request
         | 
| 194 | 
            +
                  :before_introspection_request,
         | 
| 195 | 
            +
                  :require_authorizable_account,
         | 
| 196 | 
            +
                  :oauth_tokens_unique_columns
         | 
| 183 197 | 
             
                )
         | 
| 184 198 |  | 
| 185 199 | 
             
                auth_value_methods(:only_json?)
         | 
| @@ -213,14 +227,12 @@ module Rodauth | |
| 213 227 | 
             
                end
         | 
| 214 228 |  | 
| 215 229 | 
             
                unless method_defined?(:json_request?)
         | 
| 216 | 
            -
                  # :nocov:
         | 
| 217 230 | 
             
                  # copied from the jwt feature
         | 
| 218 231 | 
             
                  def json_request?
         | 
| 219 232 | 
             
                    return @json_request if defined?(@json_request)
         | 
| 220 233 |  | 
| 221 234 | 
             
                    @json_request = request.content_type =~ json_request_regexp
         | 
| 222 235 | 
             
                  end
         | 
| 223 | 
            -
                  # :nocov:
         | 
| 224 236 | 
             
                end
         | 
| 225 237 |  | 
| 226 238 | 
             
                def initialize(scope)
         | 
| @@ -343,7 +355,7 @@ module Rodauth | |
| 343 355 | 
             
                    end
         | 
| 344 356 |  | 
| 345 357 | 
             
                    request.get do
         | 
| 346 | 
            -
                      scope.instance_variable_set(:@oauth_applications, db[ | 
| 358 | 
            +
                      scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table])
         | 
| 347 359 | 
             
                      oauth_applications_view
         | 
| 348 360 | 
             
                    end
         | 
| 349 361 |  | 
| @@ -375,8 +387,42 @@ module Rodauth | |
| 375 387 | 
             
                  end
         | 
| 376 388 | 
             
                end
         | 
| 377 389 |  | 
| 390 | 
            +
                def post_configure
         | 
| 391 | 
            +
                  super
         | 
| 392 | 
            +
                  self.class.__send__(:include, Rodauth::OAuth::ExtendDatabase(db))
         | 
| 393 | 
            +
             | 
| 394 | 
            +
                  # Check whether we can reutilize db entries for the same account / application pair
         | 
| 395 | 
            +
                  one_oauth_token_per_account = begin
         | 
| 396 | 
            +
                    db.indexes(oauth_tokens_table).values.any? do |definition|
         | 
| 397 | 
            +
                      definition[:unique] &&
         | 
| 398 | 
            +
                        definition[:columns] == oauth_tokens_unique_columns
         | 
| 399 | 
            +
                    end
         | 
| 400 | 
            +
                  end
         | 
| 401 | 
            +
                  self.class.send(:define_method, :__one_oauth_token_per_account) { one_oauth_token_per_account }
         | 
| 402 | 
            +
                end
         | 
| 403 | 
            +
             | 
| 378 404 | 
             
                private
         | 
| 379 405 |  | 
| 406 | 
            +
                def rescue_from_uniqueness_error(&block)
         | 
| 407 | 
            +
                  retries = oauth_unique_id_generation_retries
         | 
| 408 | 
            +
                  begin
         | 
| 409 | 
            +
                    transaction(savepoint: :only, &block)
         | 
| 410 | 
            +
                  rescue Sequel::UniqueConstraintViolation
         | 
| 411 | 
            +
                    redirect_response_error("already_in_use") if retries.zero?
         | 
| 412 | 
            +
                    retries -= 1
         | 
| 413 | 
            +
                    retry
         | 
| 414 | 
            +
                  end
         | 
| 415 | 
            +
                end
         | 
| 416 | 
            +
             | 
| 417 | 
            +
                # OAuth Token Unique/Reuse
         | 
| 418 | 
            +
                def oauth_tokens_unique_columns
         | 
| 419 | 
            +
                  [
         | 
| 420 | 
            +
                    oauth_tokens_oauth_application_id_column,
         | 
| 421 | 
            +
                    oauth_tokens_account_id_column,
         | 
| 422 | 
            +
                    oauth_tokens_scopes_column
         | 
| 423 | 
            +
                  ]
         | 
| 424 | 
            +
                end
         | 
| 425 | 
            +
             | 
| 380 426 | 
             
                def authorization_server_url
         | 
| 381 427 | 
             
                  base_url
         | 
| 382 428 | 
             
                end
         | 
| @@ -399,10 +445,10 @@ module Rodauth | |
| 399 445 |  | 
| 400 446 | 
             
                    # time-to-live
         | 
| 401 447 | 
             
                    ttl = if response.key?("cache-control")
         | 
| 402 | 
            -
                            cache_control = response[" | 
| 448 | 
            +
                            cache_control = response["cache-control"]
         | 
| 403 449 | 
             
                            cache_control[/max-age=(\d+)/, 1]
         | 
| 404 450 | 
             
                          elsif response.key?("expires")
         | 
| 405 | 
            -
                             | 
| 451 | 
            +
                            DateTime.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
         | 
| 406 452 | 
             
                          end
         | 
| 407 453 |  | 
| 408 454 | 
             
                    [JSON.parse(response.body, symbolize_names: true), ttl]
         | 
| @@ -482,22 +528,11 @@ module Rodauth | |
| 482 528 | 
             
                end
         | 
| 483 529 |  | 
| 484 530 | 
             
                unless method_defined?(:password_hash)
         | 
| 485 | 
            -
                  # :nocov:
         | 
| 486 531 | 
             
                  # From login_requirements_base feature
         | 
| 487 | 
            -
                  if ENV["RACK_ENV"] == "test"
         | 
| 488 | 
            -
                    def password_hash_cost
         | 
| 489 | 
            -
                      BCrypt::Engine::MIN_COST
         | 
| 490 | 
            -
                    end
         | 
| 491 | 
            -
                  else
         | 
| 492 | 
            -
                    def password_hash_cost
         | 
| 493 | 
            -
                      BCrypt::Engine::DEFAULT_COST
         | 
| 494 | 
            -
                    end
         | 
| 495 | 
            -
                  end
         | 
| 496 532 |  | 
| 497 533 | 
             
                  def password_hash(password)
         | 
| 498 | 
            -
                    BCrypt::Password.create(password, cost:  | 
| 534 | 
            +
                    BCrypt::Password.create(password, cost: BCrypt::Engine::DEFAULT_COST)
         | 
| 499 535 | 
             
                  end
         | 
| 500 | 
            -
                  # :nocov:
         | 
| 501 536 | 
             
                end
         | 
| 502 537 |  | 
| 503 538 | 
             
                def generate_oauth_token(params = {}, should_generate_refresh_token = true)
         | 
| @@ -505,43 +540,60 @@ module Rodauth | |
| 505 540 | 
             
                    oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
         | 
| 506 541 | 
             
                  }.merge(params)
         | 
| 507 542 |  | 
| 508 | 
            -
                   | 
| 509 | 
            -
             | 
| 543 | 
            +
                  rescue_from_uniqueness_error do
         | 
| 544 | 
            +
                    token = oauth_unique_id_generator
         | 
| 510 545 |  | 
| 511 | 
            -
             | 
| 512 | 
            -
             | 
| 513 | 
            -
             | 
| 514 | 
            -
             | 
| 515 | 
            -
             | 
| 546 | 
            +
                    if oauth_tokens_token_hash_column
         | 
| 547 | 
            +
                      create_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
         | 
| 548 | 
            +
                    else
         | 
| 549 | 
            +
                      create_params[oauth_tokens_token_column] = token
         | 
| 550 | 
            +
                    end
         | 
| 516 551 |  | 
| 517 | 
            -
             | 
| 518 | 
            -
                     | 
| 552 | 
            +
                    refresh_token = nil
         | 
| 553 | 
            +
                    if should_generate_refresh_token
         | 
| 554 | 
            +
                      refresh_token = oauth_unique_id_generator
         | 
| 519 555 |  | 
| 520 | 
            -
             | 
| 521 | 
            -
             | 
| 522 | 
            -
             | 
| 523 | 
            -
             | 
| 556 | 
            +
                      if oauth_tokens_refresh_token_hash_column
         | 
| 557 | 
            +
                        create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
         | 
| 558 | 
            +
                      else
         | 
| 559 | 
            +
                        create_params[oauth_tokens_refresh_token_column] = refresh_token
         | 
| 560 | 
            +
                      end
         | 
| 524 561 | 
             
                    end
         | 
| 562 | 
            +
                    oauth_token = _generate_oauth_token(create_params)
         | 
| 563 | 
            +
                    oauth_token[oauth_tokens_token_column] = token
         | 
| 564 | 
            +
                    oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
         | 
| 565 | 
            +
                    oauth_token
         | 
| 525 566 | 
             
                  end
         | 
| 526 | 
            -
                  oauth_token = _generate_oauth_token(create_params)
         | 
| 527 | 
            -
             | 
| 528 | 
            -
                  oauth_token[oauth_tokens_token_column] = token
         | 
| 529 | 
            -
                  oauth_token[oauth_tokens_refresh_token_column] = refresh_token if refresh_token
         | 
| 530 | 
            -
                  oauth_token
         | 
| 531 567 | 
             
                end
         | 
| 532 568 |  | 
| 533 569 | 
             
                def _generate_oauth_token(params = {})
         | 
| 534 570 | 
             
                  ds = db[oauth_tokens_table]
         | 
| 535 571 |  | 
| 536 | 
            -
                   | 
| 537 | 
            -
             | 
| 538 | 
            -
             | 
| 539 | 
            -
             | 
| 540 | 
            -
                       | 
| 541 | 
            -
                       | 
| 572 | 
            +
                  if __one_oauth_token_per_account
         | 
| 573 | 
            +
             | 
| 574 | 
            +
                    token = __insert_or_update_and_return__(
         | 
| 575 | 
            +
                      ds,
         | 
| 576 | 
            +
                      oauth_tokens_id_column,
         | 
| 577 | 
            +
                      oauth_tokens_unique_columns,
         | 
| 578 | 
            +
                      params,
         | 
| 579 | 
            +
                      Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP,
         | 
| 580 | 
            +
                      ([oauth_tokens_token_column, oauth_tokens_refresh_token_column] if oauth_reuse_access_token)
         | 
| 581 | 
            +
                    )
         | 
| 582 | 
            +
             | 
| 583 | 
            +
                    # if the previous operation didn't return a row, it means that the conditions
         | 
| 584 | 
            +
                    # invalidated the update, and the existing token is still valid.
         | 
| 585 | 
            +
                    token || ds.where(
         | 
| 586 | 
            +
                      oauth_tokens_account_id_column => params[oauth_tokens_account_id_column],
         | 
| 587 | 
            +
                      oauth_tokens_oauth_application_id_column => params[oauth_tokens_oauth_application_id_column]
         | 
| 588 | 
            +
                    ).first
         | 
| 589 | 
            +
                  else
         | 
| 590 | 
            +
                    if oauth_reuse_access_token
         | 
| 591 | 
            +
                      unique_conds = Hash[oauth_tokens_unique_columns.map { |column| [column, params[column]] }]
         | 
| 592 | 
            +
                      valid_token = ds.where(Sequel.expr(Sequel[oauth_tokens_table][oauth_tokens_expires_in_column]) > Sequel::CURRENT_TIMESTAMP)
         | 
| 593 | 
            +
                                      .where(unique_conds).first
         | 
| 594 | 
            +
                      return valid_token if valid_token
         | 
| 542 595 | 
             
                    end
         | 
| 543 | 
            -
             | 
| 544 | 
            -
                    retry
         | 
| 596 | 
            +
                    __insert_and_return__(ds, oauth_tokens_id_column, params)
         | 
| 545 597 | 
             
                  end
         | 
| 546 598 | 
             
                end
         | 
| 547 599 |  | 
| @@ -633,7 +685,6 @@ module Rodauth | |
| 633 685 | 
             
                  # set client ID/secret pairs
         | 
| 634 686 |  | 
| 635 687 | 
             
                  create_params.merge! \
         | 
| 636 | 
            -
                    oauth_applications_client_id_column => oauth_unique_id_generator,
         | 
| 637 688 | 
             
                    oauth_applications_client_secret_column => \
         | 
| 638 689 | 
             
                      secret_hash(oauth_application_params[oauth_application_client_secret_param])
         | 
| 639 690 |  | 
| @@ -643,29 +694,14 @@ module Rodauth | |
| 643 694 | 
             
                                                                      oauth_application_default_scope
         | 
| 644 695 | 
             
                                                                    end
         | 
| 645 696 |  | 
| 646 | 
            -
                   | 
| 647 | 
            -
             | 
| 648 | 
            -
             | 
| 649 | 
            -
                             false
         | 
| 650 | 
            -
                           rescue Sequel::ConstraintViolation => e
         | 
| 651 | 
            -
                             e
         | 
| 652 | 
            -
                           end
         | 
| 653 | 
            -
             | 
| 654 | 
            -
                  if raised
         | 
| 655 | 
            -
                    field = raised.message[/\.(.*)$/, 1]
         | 
| 656 | 
            -
                    case raised
         | 
| 657 | 
            -
                    when Sequel::UniqueConstraintViolation
         | 
| 658 | 
            -
                      throw_error(field, unique_error_message)
         | 
| 659 | 
            -
                    when Sequel::NotNullConstraintViolation
         | 
| 660 | 
            -
                      throw_error(field, null_error_message)
         | 
| 661 | 
            -
                    end
         | 
| 697 | 
            +
                  rescue_from_uniqueness_error do
         | 
| 698 | 
            +
                    create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
         | 
| 699 | 
            +
                    db[oauth_applications_table].insert(create_params)
         | 
| 662 700 | 
             
                  end
         | 
| 663 | 
            -
             | 
| 664 | 
            -
                  !raised && id
         | 
| 665 701 | 
             
                end
         | 
| 666 702 |  | 
| 667 703 | 
             
                # Authorize
         | 
| 668 | 
            -
                def  | 
| 704 | 
            +
                def require_authorizable_account
         | 
| 669 705 | 
             
                  require_account
         | 
| 670 706 | 
             
                end
         | 
| 671 707 |  | 
| @@ -709,35 +745,25 @@ module Rodauth | |
| 709 745 | 
             
                  )
         | 
| 710 746 |  | 
| 711 747 | 
             
                  # Access Type flow
         | 
| 712 | 
            -
                  if use_oauth_access_type?
         | 
| 713 | 
            -
                     | 
| 714 | 
            -
                      create_params[oauth_grants_access_type_column] = access_type
         | 
| 715 | 
            -
                    end
         | 
| 748 | 
            +
                  if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
         | 
| 749 | 
            +
                    create_params[oauth_grants_access_type_column] = access_type
         | 
| 716 750 | 
             
                  end
         | 
| 717 751 |  | 
| 718 752 | 
             
                  # PKCE flow
         | 
| 719 | 
            -
                  if use_oauth_pkce?
         | 
| 753 | 
            +
                  if use_oauth_pkce? && (code_challenge = param_or_nil("code_challenge"))
         | 
| 754 | 
            +
                    code_challenge_method = param_or_nil("code_challenge_method")
         | 
| 720 755 |  | 
| 721 | 
            -
                     | 
| 722 | 
            -
             | 
| 723 | 
            -
             | 
| 724 | 
            -
                      create_params[oauth_grants_code_challenge_column] = code_challenge
         | 
| 725 | 
            -
                      create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
         | 
| 726 | 
            -
                    elsif oauth_require_pkce
         | 
| 727 | 
            -
                      redirect_response_error("code_challenge_required")
         | 
| 728 | 
            -
                    end
         | 
| 756 | 
            +
                    create_params[oauth_grants_code_challenge_column] = code_challenge
         | 
| 757 | 
            +
                    create_params[oauth_grants_code_challenge_method_column] = code_challenge_method
         | 
| 729 758 | 
             
                  end
         | 
| 730 759 |  | 
| 731 760 | 
             
                  ds = db[oauth_grants_table]
         | 
| 732 761 |  | 
| 733 | 
            -
                   | 
| 734 | 
            -
                     | 
| 735 | 
            -
                     | 
| 736 | 
            -
                    ds.insert(create_params)
         | 
| 737 | 
            -
                    authorization_code
         | 
| 738 | 
            -
                  rescue Sequel::UniqueConstraintViolation
         | 
| 739 | 
            -
                    retry
         | 
| 762 | 
            +
                  rescue_from_uniqueness_error do
         | 
| 763 | 
            +
                    create_params[oauth_grants_code_column] = oauth_unique_id_generator
         | 
| 764 | 
            +
                    __insert_and_return__(ds, oauth_grants_id_column, create_params)
         | 
| 740 765 | 
             
                  end
         | 
| 766 | 
            +
                  create_params[oauth_grants_code_column]
         | 
| 741 767 | 
             
                end
         | 
| 742 768 |  | 
| 743 769 | 
             
                def do_authorize(redirect_url, query_params = [], fragment_params = [])
         | 
| @@ -781,10 +807,6 @@ module Rodauth | |
| 781 807 |  | 
| 782 808 | 
             
                # Access Tokens
         | 
| 783 809 |  | 
| 784 | 
            -
                def before_token
         | 
| 785 | 
            -
                  require_oauth_application
         | 
| 786 | 
            -
                end
         | 
| 787 | 
            -
             | 
| 788 810 | 
             
                def validate_oauth_token_params
         | 
| 789 811 | 
             
                  unless (grant_type = param_or_nil("grant_type"))
         | 
| 790 812 | 
             
                    redirect_response_error("invalid_request")
         | 
| @@ -834,8 +856,6 @@ module Rodauth | |
| 834 856 | 
             
                      oauth_tokens_expires_in_column => Time.now + oauth_token_expires_in
         | 
| 835 857 | 
             
                    }
         | 
| 836 858 | 
             
                    create_oauth_token_from_token(oauth_token, update_params)
         | 
| 837 | 
            -
                  else
         | 
| 838 | 
            -
                    redirect_response_error("invalid_grant")
         | 
| 839 859 | 
             
                  end
         | 
| 840 860 | 
             
                end
         | 
| 841 861 |  | 
| @@ -864,29 +884,21 @@ module Rodauth | |
| 864 884 | 
             
                def create_oauth_token_from_token(oauth_token, update_params)
         | 
| 865 885 | 
             
                  redirect_response_error("invalid_grant") unless token_from_application?(oauth_token, oauth_application)
         | 
| 866 886 |  | 
| 867 | 
            -
                   | 
| 868 | 
            -
             | 
| 869 | 
            -
                  if oauth_tokens_token_hash_column
         | 
| 870 | 
            -
                    update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
         | 
| 871 | 
            -
                  else
         | 
| 872 | 
            -
                    update_params[oauth_tokens_token_column] = token
         | 
| 873 | 
            -
                  end
         | 
| 887 | 
            +
                  rescue_from_uniqueness_error do
         | 
| 888 | 
            +
                    token = oauth_unique_id_generator
         | 
| 874 889 |  | 
| 875 | 
            -
             | 
| 876 | 
            -
             | 
| 877 | 
            -
                  oauth_token = begin
         | 
| 878 | 
            -
                    if ds.supports_returning?(:update)
         | 
| 879 | 
            -
                      ds.returning.update(update_params).first
         | 
| 890 | 
            +
                    if oauth_tokens_token_hash_column
         | 
| 891 | 
            +
                      update_params[oauth_tokens_token_hash_column] = generate_token_hash(token)
         | 
| 880 892 | 
             
                    else
         | 
| 881 | 
            -
                       | 
| 882 | 
            -
                      ds.first
         | 
| 893 | 
            +
                      update_params[oauth_tokens_token_column] = token
         | 
| 883 894 | 
             
                    end
         | 
| 884 | 
            -
                                rescue Sequel::UniqueConstraintViolation
         | 
| 885 | 
            -
                                  retry
         | 
| 886 | 
            -
                  end
         | 
| 887 895 |  | 
| 888 | 
            -
             | 
| 889 | 
            -
             | 
| 896 | 
            +
                    ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
         | 
| 897 | 
            +
             | 
| 898 | 
            +
                    oauth_token = __update_and_return__(ds, update_params)
         | 
| 899 | 
            +
                    oauth_token[oauth_tokens_token_column] = token
         | 
| 900 | 
            +
                    oauth_token
         | 
| 901 | 
            +
                  end
         | 
| 890 902 | 
             
                end
         | 
| 891 903 |  | 
| 892 904 | 
             
                TOKEN_HINT_TYPES = %w[access_token refresh_token].freeze
         | 
| @@ -895,8 +907,8 @@ module Rodauth | |
| 895 907 |  | 
| 896 908 | 
             
                def validate_oauth_introspect_params
         | 
| 897 909 | 
             
                  # check if valid token hint type
         | 
| 898 | 
            -
                  if param_or_nil("token_type_hint")
         | 
| 899 | 
            -
                    redirect_response_error("unsupported_token_type") | 
| 910 | 
            +
                  if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
         | 
| 911 | 
            +
                    redirect_response_error("unsupported_token_type")
         | 
| 900 912 | 
             
                  end
         | 
| 901 913 |  | 
| 902 914 | 
             
                  redirect_response_error("invalid_request") unless param_or_nil("token")
         | 
| @@ -914,18 +926,12 @@ module Rodauth | |
| 914 926 | 
             
                  }
         | 
| 915 927 | 
             
                end
         | 
| 916 928 |  | 
| 917 | 
            -
                def before_introspect; end
         | 
| 918 | 
            -
             | 
| 919 929 | 
             
                # Token revocation
         | 
| 920 930 |  | 
| 921 | 
            -
                def before_revoke
         | 
| 922 | 
            -
                  require_oauth_application
         | 
| 923 | 
            -
                end
         | 
| 924 | 
            -
             | 
| 925 931 | 
             
                def validate_oauth_revoke_params
         | 
| 926 932 | 
             
                  # check if valid token hint type
         | 
| 927 | 
            -
                  if param_or_nil("token_type_hint")
         | 
| 928 | 
            -
                    redirect_response_error("unsupported_token_type") | 
| 933 | 
            +
                  if param_or_nil("token_type_hint") && !TOKEN_HINT_TYPES.include?(param("token_type_hint"))
         | 
| 934 | 
            +
                    redirect_response_error("unsupported_token_type")
         | 
| 929 935 | 
             
                  end
         | 
| 930 936 |  | 
| 931 937 | 
             
                  redirect_response_error("invalid_request") unless param_or_nil("token")
         | 
| @@ -942,23 +948,13 @@ module Rodauth | |
| 942 948 |  | 
| 943 949 | 
             
                  redirect_response_error("invalid_request") unless oauth_token
         | 
| 944 950 |  | 
| 945 | 
            -
                   | 
| 946 | 
            -
                    redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
         | 
| 947 | 
            -
                  else
         | 
| 948 | 
            -
                    @oauth_application = db[oauth_applications_table].where(oauth_applications_id_column =>
         | 
| 949 | 
            -
                      oauth_token[oauth_tokens_oauth_application_id_column]).first
         | 
| 950 | 
            -
                  end
         | 
| 951 | 
            +
                  redirect_response_error("invalid_request") unless token_from_application?(oauth_token, oauth_application)
         | 
| 951 952 |  | 
| 952 953 | 
             
                  update_params = { oauth_tokens_revoked_at_column => Sequel::CURRENT_TIMESTAMP }
         | 
| 953 954 |  | 
| 954 955 | 
             
                  ds = db[oauth_tokens_table].where(oauth_tokens_id_column => oauth_token[oauth_tokens_id_column])
         | 
| 955 956 |  | 
| 956 | 
            -
                  oauth_token =  | 
| 957 | 
            -
                                  ds.returning.update(update_params).first
         | 
| 958 | 
            -
                                else
         | 
| 959 | 
            -
                                  ds.update(update_params)
         | 
| 960 | 
            -
                                  ds.first
         | 
| 961 | 
            -
                                end
         | 
| 957 | 
            +
                  oauth_token = __update_and_return__(ds, update_params)
         | 
| 962 958 |  | 
| 963 959 | 
             
                  oauth_token[oauth_tokens_token_column] = token
         | 
| 964 960 | 
             
                  oauth_token
         | 
| @@ -976,7 +972,13 @@ module Rodauth | |
| 976 972 |  | 
| 977 973 | 
             
                def redirect_response_error(error_code, redirect_url = redirect_uri || request.referer || default_redirect)
         | 
| 978 974 | 
             
                  if accepts_json?
         | 
| 979 | 
            -
                     | 
| 975 | 
            +
                    status_code = if respond_to?(:"#{error_code}_response_status")
         | 
| 976 | 
            +
                                    send(:"#{error_code}_response_status")
         | 
| 977 | 
            +
                                  else
         | 
| 978 | 
            +
                                    invalid_oauth_response_status
         | 
| 979 | 
            +
                                  end
         | 
| 980 | 
            +
             | 
| 981 | 
            +
                    throw_json_response_error(status_code, error_code)
         | 
| 980 982 | 
             
                  else
         | 
| 981 983 | 
             
                    redirect_url = URI.parse(redirect_url)
         | 
| 982 984 | 
             
                    query_params = []
         | 
| @@ -1023,7 +1025,6 @@ module Rodauth | |
| 1023 1025 | 
             
                end
         | 
| 1024 1026 |  | 
| 1025 1027 | 
             
                unless method_defined?(:_json_response_body)
         | 
| 1026 | 
            -
                  # :nocov:
         | 
| 1027 1028 | 
             
                  def _json_response_body(hash)
         | 
| 1028 1029 | 
             
                    if request.respond_to?(:convert_to_json)
         | 
| 1029 1030 | 
             
                      request.send(:convert_to_json, hash)
         | 
| @@ -1031,7 +1032,6 @@ module Rodauth | |
| 1031 1032 | 
             
                      JSON.dump(hash)
         | 
| 1032 1033 | 
             
                    end
         | 
| 1033 1034 | 
             
                  end
         | 
| 1034 | 
            -
                  # :nocov:
         | 
| 1035 1035 | 
             
                end
         | 
| 1036 1036 |  | 
| 1037 1037 | 
             
                def authorization_required
         | 
| @@ -1156,7 +1156,8 @@ module Rodauth | |
| 1156 1156 | 
             
                route(:token) do |r|
         | 
| 1157 1157 | 
             
                  next unless is_authorization_server?
         | 
| 1158 1158 |  | 
| 1159 | 
            -
                   | 
| 1159 | 
            +
                  before_token_route
         | 
| 1160 | 
            +
                  require_oauth_application
         | 
| 1160 1161 |  | 
| 1161 1162 | 
             
                  r.post do
         | 
| 1162 1163 | 
             
                    catch_error do
         | 
| @@ -1164,6 +1165,7 @@ module Rodauth | |
| 1164 1165 |  | 
| 1165 1166 | 
             
                      oauth_token = nil
         | 
| 1166 1167 | 
             
                      transaction do
         | 
| 1168 | 
            +
                        before_token
         | 
| 1167 1169 | 
             
                        oauth_token = create_oauth_token
         | 
| 1168 1170 | 
             
                      end
         | 
| 1169 1171 |  | 
| @@ -1178,12 +1180,13 @@ module Rodauth | |
| 1178 1180 | 
             
                route(:introspect) do |r|
         | 
| 1179 1181 | 
             
                  next unless is_authorization_server?
         | 
| 1180 1182 |  | 
| 1181 | 
            -
                   | 
| 1183 | 
            +
                  before_introspect_route
         | 
| 1182 1184 |  | 
| 1183 1185 | 
             
                  r.post do
         | 
| 1184 1186 | 
             
                    catch_error do
         | 
| 1185 1187 | 
             
                      validate_oauth_introspect_params
         | 
| 1186 1188 |  | 
| 1189 | 
            +
                      before_introspect
         | 
| 1187 1190 | 
             
                      oauth_token = case param("token_type_hint")
         | 
| 1188 1191 | 
             
                                    when "access_token"
         | 
| 1189 1192 | 
             
                                      oauth_token_by_token(param("token"))
         | 
| @@ -1211,7 +1214,8 @@ module Rodauth | |
| 1211 1214 | 
             
                route(:revoke) do |r|
         | 
| 1212 1215 | 
             
                  next unless is_authorization_server?
         | 
| 1213 1216 |  | 
| 1214 | 
            -
                   | 
| 1217 | 
            +
                  before_revoke_route
         | 
| 1218 | 
            +
                  require_oauth_application
         | 
| 1215 1219 |  | 
| 1216 1220 | 
             
                  r.post do
         | 
| 1217 1221 | 
             
                    catch_error do
         | 
| @@ -1219,6 +1223,7 @@ module Rodauth | |
| 1219 1223 |  | 
| 1220 1224 | 
             
                      oauth_token = nil
         | 
| 1221 1225 | 
             
                      transaction do
         | 
| 1226 | 
            +
                        before_revoke
         | 
| 1222 1227 | 
             
                        oauth_token = revoke_oauth_token
         | 
| 1223 1228 | 
             
                        after_revoke
         | 
| 1224 1229 | 
             
                      end
         | 
| @@ -1242,12 +1247,12 @@ module Rodauth | |
| 1242 1247 | 
             
                route(:authorize) do |r|
         | 
| 1243 1248 | 
             
                  next unless is_authorization_server?
         | 
| 1244 1249 |  | 
| 1245 | 
            -
                   | 
| 1250 | 
            +
                  before_authorize_route
         | 
| 1251 | 
            +
                  require_authorizable_account
         | 
| 1252 | 
            +
             | 
| 1246 1253 | 
             
                  validate_oauth_grant_params
         | 
| 1247 1254 | 
             
                  try_approval_prompt if use_oauth_access_type? && request.get?
         | 
| 1248 1255 |  | 
| 1249 | 
            -
                  before_authorize
         | 
| 1250 | 
            -
             | 
| 1251 1256 | 
             
                  r.get do
         | 
| 1252 1257 | 
             
                    authorize_view
         | 
| 1253 1258 | 
             
                  end
         | 
| @@ -1256,6 +1261,7 @@ module Rodauth | |
| 1256 1261 | 
             
                    redirect_url = URI.parse(redirect_uri)
         | 
| 1257 1262 |  | 
| 1258 1263 | 
             
                    transaction do
         | 
| 1264 | 
            +
                      before_authorize
         | 
| 1259 1265 | 
             
                      do_authorize(redirect_url)
         | 
| 1260 1266 | 
             
                    end
         | 
| 1261 1267 | 
             
                    redirect(redirect_url.to_s)
         | 
| @@ -2,7 +2,6 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module Rodauth
         | 
| 4 4 | 
             
              Feature.define(:oauth_http_mac) do
         | 
| 5 | 
            -
                # :nocov:
         | 
| 6 5 | 
             
                unless String.method_defined?(:delete_prefix)
         | 
| 7 6 | 
             
                  module PrefixExtensions
         | 
| 8 7 | 
             
                    refine(String) do
         | 
| @@ -28,7 +27,6 @@ module Rodauth | |
| 28 27 | 
             
                  end
         | 
| 29 28 | 
             
                  using(PrefixExtensions)
         | 
| 30 29 | 
             
                end
         | 
| 31 | 
            -
                # :nocov:
         | 
| 32 30 |  | 
| 33 31 | 
             
                depends :oauth
         | 
| 34 32 |  | 
| @@ -22,6 +22,10 @@ module Rodauth | |
| 22 22 | 
             
                auth_value_method :oauth_jwt_jwe_algorithm, nil
         | 
| 23 23 | 
             
                auth_value_method :oauth_jwt_jwe_encryption_method, nil
         | 
| 24 24 |  | 
| 25 | 
            +
                # values used for rotating keys
         | 
| 26 | 
            +
                auth_value_method :oauth_jwt_legacy_public_key, nil
         | 
| 27 | 
            +
                auth_value_method :oauth_jwt_legacy_algorithm, nil
         | 
| 28 | 
            +
             | 
| 25 29 | 
             
                auth_value_method :oauth_jwt_jwe_copyright, nil
         | 
| 26 30 | 
             
                auth_value_method :oauth_jwt_audience, nil
         | 
| 27 31 |  | 
| @@ -88,9 +92,7 @@ module Rodauth | |
| 88 92 | 
             
                  jws_jwk = if oauth_application[oauth_application_jws_jwk_column]
         | 
| 89 93 | 
             
                              jwk = oauth_application[oauth_application_jws_jwk_column]
         | 
| 90 94 |  | 
| 91 | 
            -
                              if jwk
         | 
| 92 | 
            -
                                jwk = JSON.parse(jwk, symbolize_names: true) if jwk.is_a?(String)
         | 
| 93 | 
            -
                              end
         | 
| 95 | 
            +
                              jwk = JSON.parse(jwk, symbolize_names: true) if jwk && jwk.is_a?(String)
         | 
| 94 96 | 
             
                            else
         | 
| 95 97 | 
             
                              redirect_response_error("invalid_request_object")
         | 
| 96 98 | 
             
                            end
         | 
| @@ -105,8 +107,8 @@ module Rodauth | |
| 105 107 | 
             
                  # [RFC7519] specification.  The value of "aud" should be the value of
         | 
| 106 108 | 
             
                  # the Authorization Server (AS) "issuer" as defined in RFC8414
         | 
| 107 109 | 
             
                  # [RFC8414].
         | 
| 108 | 
            -
                  claims.delete( | 
| 109 | 
            -
                  audience = claims.delete( | 
| 110 | 
            +
                  claims.delete("iss")
         | 
| 111 | 
            +
                  audience = claims.delete("aud")
         | 
| 110 112 |  | 
| 111 113 | 
             
                  redirect_response_error("invalid_request_object") if audience && audience != authorization_server_url
         | 
| 112 114 |  | 
| @@ -119,11 +121,17 @@ module Rodauth | |
| 119 121 |  | 
| 120 122 | 
             
                # /token
         | 
| 121 123 |  | 
| 122 | 
            -
                def  | 
| 124 | 
            +
                def require_oauth_application
         | 
| 123 125 | 
             
                  # requset authentication optional for assertions
         | 
| 124 | 
            -
                  return  | 
| 126 | 
            +
                  return super unless param("grant_type") == "urn:ietf:params:oauth:grant-type:jwt-bearer"
         | 
| 125 127 |  | 
| 126 | 
            -
                   | 
| 128 | 
            +
                  claims = jwt_decode(param("assertion"))
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  redirect_response_error("invalid_grant") unless claims
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                  @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  authorization_required unless @oauth_application
         | 
| 127 135 | 
             
                end
         | 
| 128 136 |  | 
| 129 137 | 
             
                def validate_oauth_token_params
         | 
| @@ -145,10 +153,6 @@ module Rodauth | |
| 145 153 | 
             
                def create_oauth_token_from_assertion
         | 
| 146 154 | 
             
                  claims = jwt_decode(param("assertion"))
         | 
| 147 155 |  | 
| 148 | 
            -
                  redirect_response_error("invalid_grant") unless claims
         | 
| 149 | 
            -
             | 
| 150 | 
            -
                  @oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
         | 
| 151 | 
            -
             | 
| 152 156 | 
             
                  account = account_ds(claims["sub"]).first
         | 
| 153 157 |  | 
| 154 158 | 
             
                  redirect_response_error("invalid_client") unless oauth_application && account
         | 
| @@ -167,17 +171,19 @@ module Rodauth | |
| 167 171 | 
             
                    oauth_grants_expires_in_column => Time.now + oauth_token_expires_in
         | 
| 168 172 | 
             
                  }.merge(params)
         | 
| 169 173 |  | 
| 170 | 
            -
                   | 
| 171 | 
            -
                     | 
| 174 | 
            +
                  oauth_token = rescue_from_uniqueness_error do
         | 
| 175 | 
            +
                    if should_generate_refresh_token
         | 
| 176 | 
            +
                      refresh_token = oauth_unique_id_generator
         | 
| 172 177 |  | 
| 173 | 
            -
             | 
| 174 | 
            -
             | 
| 175 | 
            -
             | 
| 176 | 
            -
             | 
| 178 | 
            +
                      if oauth_tokens_refresh_token_hash_column
         | 
| 179 | 
            +
                        create_params[oauth_tokens_refresh_token_hash_column] = generate_token_hash(refresh_token)
         | 
| 180 | 
            +
                      else
         | 
| 181 | 
            +
                        create_params[oauth_tokens_refresh_token_column] = refresh_token
         | 
| 182 | 
            +
                      end
         | 
| 177 183 | 
             
                    end
         | 
| 178 | 
            -
                  end
         | 
| 179 184 |  | 
| 180 | 
            -
             | 
| 185 | 
            +
                    _generate_oauth_token(create_params)
         | 
| 186 | 
            +
                  end
         | 
| 181 187 |  | 
| 182 188 | 
             
                  claims = jwt_claims(oauth_token)
         | 
| 183 189 |  | 
| @@ -293,10 +299,10 @@ module Rodauth | |
| 293 299 |  | 
| 294 300 | 
             
                    # time-to-live
         | 
| 295 301 | 
             
                    ttl = if response.key?("cache-control")
         | 
| 296 | 
            -
                            cache_control = response[" | 
| 302 | 
            +
                            cache_control = response["cache-control"]
         | 
| 297 303 | 
             
                            cache_control[/max-age=(\d+)/, 1]
         | 
| 298 304 | 
             
                          elsif response.key?("expires")
         | 
| 299 | 
            -
                             | 
| 305 | 
            +
                            DateTime.httpdate(response["expires"]).utc.to_i - Time.now.utc.to_i
         | 
| 300 306 | 
             
                          end
         | 
| 301 307 |  | 
| 302 308 | 
             
                    [JSON.parse(response.body, symbolize_names: true), ttl]
         | 
| @@ -304,7 +310,6 @@ module Rodauth | |
| 304 310 | 
             
                end
         | 
| 305 311 |  | 
| 306 312 | 
             
                if defined?(JSON::JWT)
         | 
| 307 | 
            -
                  # :nocov:
         | 
| 308 313 |  | 
| 309 314 | 
             
                  def jwk_import(data)
         | 
| 310 315 | 
             
                    JSON::JWK.new(data)
         | 
| @@ -330,23 +335,27 @@ module Rodauth | |
| 330 335 | 
             
                  def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, **)
         | 
| 331 336 | 
             
                    token = JSON::JWT.decode(token, oauth_jwt_jwe_key).plain_text if oauth_jwt_jwe_key
         | 
| 332 337 |  | 
| 333 | 
            -
                     | 
| 334 | 
            -
             | 
| 335 | 
            -
             | 
| 336 | 
            -
             | 
| 337 | 
            -
             | 
| 338 | 
            +
                    if is_authorization_server?
         | 
| 339 | 
            +
                      if oauth_jwt_legacy_public_key
         | 
| 340 | 
            +
                        JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks_set }))
         | 
| 341 | 
            +
                      elsif jws_key
         | 
| 342 | 
            +
                        JSON::JWT.decode(token, jws_key)
         | 
| 343 | 
            +
                      end
         | 
| 344 | 
            +
                    elsif (jwks = auth_server_jwks_set)
         | 
| 345 | 
            +
                      JSON::JWT.decode(token, JSON::JWK::Set.new(jwks))
         | 
| 346 | 
            +
                    end
         | 
| 338 347 | 
             
                  rescue JSON::JWT::Exception
         | 
| 339 348 | 
             
                    nil
         | 
| 340 349 | 
             
                  end
         | 
| 341 350 |  | 
| 342 351 | 
             
                  def jwks_set
         | 
| 343 | 
            -
                    [
         | 
| 352 | 
            +
                    @jwks_set ||= [
         | 
| 344 353 | 
             
                      (JSON::JWK.new(oauth_jwt_public_key).merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
         | 
| 354 | 
            +
                      (JSON::JWK.new(oauth_jwt_legacy_public_key).merge(use: "sig", alg: oauth_jwt_legacy_algorithm) if oauth_jwt_legacy_public_key),
         | 
| 345 355 | 
             
                      (JSON::JWK.new(oauth_jwt_jwe_public_key).merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
         | 
| 346 356 | 
             
                    ].compact
         | 
| 347 357 | 
             
                  end
         | 
| 348 358 |  | 
| 349 | 
            -
                  # :nocov:
         | 
| 350 359 | 
             
                elsif defined?(JWT)
         | 
| 351 360 |  | 
| 352 361 | 
             
                  # ruby-jwt
         | 
| @@ -391,21 +400,30 @@ module Rodauth | |
| 391 400 | 
             
                  def jwt_decode(token, jws_key: oauth_jwt_public_key || _jwt_key, jws_algorithm: oauth_jwt_algorithm)
         | 
| 392 401 | 
             
                    # decrypt jwe
         | 
| 393 402 | 
             
                    token = JWE.decrypt(token, oauth_jwt_jwe_key) if oauth_jwt_jwe_key
         | 
| 394 | 
            -
             | 
| 395 403 | 
             
                    # decode jwt
         | 
| 396 | 
            -
                     | 
| 397 | 
            -
             | 
| 398 | 
            -
             | 
| 399 | 
            -
             | 
| 400 | 
            -
             | 
| 401 | 
            -
             | 
| 404 | 
            +
                    if is_authorization_server?
         | 
| 405 | 
            +
                      if oauth_jwt_legacy_public_key
         | 
| 406 | 
            +
                        algorithms = jwks_set.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
         | 
| 407 | 
            +
                        JWT.decode(token, nil, true, jwks: { keys: jwks_set }, algorithms: algorithms).first
         | 
| 408 | 
            +
                      elsif jws_key
         | 
| 409 | 
            +
                        JWT.decode(token, jws_key, true, algorithms: [jws_algorithm]).first
         | 
| 410 | 
            +
                      end
         | 
| 411 | 
            +
                    elsif (jwks = auth_server_jwks_set)
         | 
| 412 | 
            +
                      algorithms = jwks[:keys].select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
         | 
| 413 | 
            +
                      JWT.decode(token, nil, true, jwks: jwks, algorithms: algorithms).first
         | 
| 414 | 
            +
                    end
         | 
| 402 415 | 
             
                  rescue JWT::DecodeError, JWT::JWKError
         | 
| 403 416 | 
             
                    nil
         | 
| 404 417 | 
             
                  end
         | 
| 405 418 |  | 
| 406 419 | 
             
                  def jwks_set
         | 
| 407 | 
            -
                    [
         | 
| 420 | 
            +
                    @jwks_set ||= [
         | 
| 408 421 | 
             
                      (JWT::JWK.new(oauth_jwt_public_key).export.merge(use: "sig", alg: oauth_jwt_algorithm) if oauth_jwt_public_key),
         | 
| 422 | 
            +
                      (
         | 
| 423 | 
            +
                         if oauth_jwt_legacy_public_key
         | 
| 424 | 
            +
                           JWT::JWK.new(oauth_jwt_legacy_public_key).export.merge(use: "sig", alg: oauth_jwt_legacy_algorithm)
         | 
| 425 | 
            +
                         end
         | 
| 426 | 
            +
                       ),
         | 
| 409 427 | 
             
                      (JWT::JWK.new(oauth_jwt_jwe_public_key).export.merge(use: "enc", alg: oauth_jwt_jwe_algorithm) if oauth_jwt_jwe_public_key)
         | 
| 410 428 | 
             
                    ].compact
         | 
| 411 429 | 
             
                  end
         | 
| @@ -0,0 +1,104 @@ | |
| 1 | 
            +
            # frozen-string-literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "onelogin/ruby-saml"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Rodauth
         | 
| 6 | 
            +
              Feature.define(:oauth_saml) do
         | 
| 7 | 
            +
                depends :oauth
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                auth_value_method :oauth_saml_cert_fingerprint, "9E:65:2E:03:06:8D:80:F2:86:C7:6C:77:A1:D9:14:97:0A:4D:F4:4D"
         | 
| 10 | 
            +
                auth_value_method :oauth_saml_cert_fingerprint_algorithm, nil
         | 
| 11 | 
            +
                auth_value_method :oauth_saml_name_identifier_format, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                auth_value_method :oauth_saml_security_authn_requests_signed, false
         | 
| 14 | 
            +
                auth_value_method :oauth_saml_security_metadata_signed, false
         | 
| 15 | 
            +
                auth_value_method :oauth_saml_security_digest_method, XMLSecurity::Document::SHA1
         | 
| 16 | 
            +
                auth_value_method :oauth_saml_security_signature_method, XMLSecurity::Document::RSA_SHA1
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                SAML_GRANT_TYPE = "http://oauth.net/grant_type/assertion/saml/2.0/bearer"
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                # /token
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def require_oauth_application
         | 
| 23 | 
            +
                  # requset authentication optional for assertions
         | 
| 24 | 
            +
                  return super unless param("grant_type") == SAML_GRANT_TYPE && !param_or_nil("client_id")
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  # TODO: invalid grant
         | 
| 27 | 
            +
                  authorization_required unless saml_assertion
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  redirect_uri = saml_assertion.destination
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  @oauth_application = db[oauth_applications_table].where(
         | 
| 32 | 
            +
                    oauth_applications_homepage_url_column => saml_assertion.audiences,
         | 
| 33 | 
            +
                    oauth_applications_redirect_uri_column => redirect_uri
         | 
| 34 | 
            +
                  ).first
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  # The Assertion's <Issuer> element MUST contain a unique identifier
         | 
| 37 | 
            +
                  # for the entity that issued the Assertion.
         | 
| 38 | 
            +
                  authorization_required unless saml_assertion.issuers.all? do |issuer|
         | 
| 39 | 
            +
                    issuer.start_with?(@oauth_application[oauth_applications_homepage_url_column])
         | 
| 40 | 
            +
                  end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  authorization_required unless @oauth_application
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                private
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def secret_matches?(oauth_application, secret)
         | 
| 48 | 
            +
                  return super unless param_or_nil("assertion")
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  true
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def saml_assertion
         | 
| 54 | 
            +
                  return @saml_assertion if defined?(@saml_assertion)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  @saml_assertion = begin
         | 
| 57 | 
            +
                    settings = OneLogin::RubySaml::Settings.new
         | 
| 58 | 
            +
                    settings.idp_cert_fingerprint = oauth_saml_cert_fingerprint
         | 
| 59 | 
            +
                    settings.idp_cert_fingerprint_algorithm = oauth_saml_cert_fingerprint_algorithm
         | 
| 60 | 
            +
                    settings.name_identifier_format = oauth_saml_name_identifier_format
         | 
| 61 | 
            +
                    settings.security[:authn_requests_signed] = oauth_saml_security_authn_requests_signed
         | 
| 62 | 
            +
                    settings.security[:metadata_signed] = oauth_saml_security_metadata_signed
         | 
| 63 | 
            +
                    settings.security[:digest_method] = oauth_saml_security_digest_method
         | 
| 64 | 
            +
                    settings.security[:signature_method] = oauth_saml_security_signature_method
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                    response = OneLogin::RubySaml::Response.new(param("assertion"), settings: settings, skip_recipient_check: true)
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                    return unless response.is_valid?
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    response
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def validate_oauth_token_params
         | 
| 75 | 
            +
                  return super unless param("grant_type") == SAML_GRANT_TYPE
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  redirect_response_error("invalid_client") unless param_or_nil("assertion")
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  redirect_response_error("invalid_scope") unless check_valid_scopes?
         | 
| 80 | 
            +
                end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                def create_oauth_token
         | 
| 83 | 
            +
                  if param("grant_type") == SAML_GRANT_TYPE
         | 
| 84 | 
            +
                    create_oauth_token_from_saml_assertion
         | 
| 85 | 
            +
                  else
         | 
| 86 | 
            +
                    super
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                def create_oauth_token_from_saml_assertion
         | 
| 91 | 
            +
                  account = db[accounts_table].where(login_column => saml_assertion.nameid).first
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  redirect_response_error("invalid_client") unless oauth_application && account
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  create_params = {
         | 
| 96 | 
            +
                    oauth_tokens_account_id_column => account[account_id_column],
         | 
| 97 | 
            +
                    oauth_tokens_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 98 | 
            +
                    oauth_tokens_scopes_column => (param_or_nil("scope") || oauth_application[oauth_applications_scopes_column])
         | 
| 99 | 
            +
                  }
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                  generate_oauth_token(create_params, false)
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
              end
         | 
| 104 | 
            +
            end
         | 
| @@ -0,0 +1,73 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Rodauth
         | 
| 4 | 
            +
              module OAuth
         | 
| 5 | 
            +
                # rubocop:disable Naming/MethodName, Metrics/ParameterLists
         | 
| 6 | 
            +
                def self.ExtendDatabase(db)
         | 
| 7 | 
            +
                  Module.new do
         | 
| 8 | 
            +
                    dataset = db.dataset
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                    if dataset.supports_returning?(:insert)
         | 
| 11 | 
            +
                      def __insert_and_return__(dataset, _pkey, params)
         | 
| 12 | 
            +
                        dataset.returning.insert(params).first
         | 
| 13 | 
            +
                      end
         | 
| 14 | 
            +
                    else
         | 
| 15 | 
            +
                      def __insert_and_return__(dataset, pkey, params)
         | 
| 16 | 
            +
                        id = dataset.insert(params)
         | 
| 17 | 
            +
                        dataset.where(pkey => id).first
         | 
| 18 | 
            +
                      end
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    if dataset.supports_returning?(:update)
         | 
| 22 | 
            +
                      def __update_and_return__(dataset, params)
         | 
| 23 | 
            +
                        dataset.returning.update(params).first
         | 
| 24 | 
            +
                      end
         | 
| 25 | 
            +
                    else
         | 
| 26 | 
            +
                      def __update_and_return__(dataset, params)
         | 
| 27 | 
            +
                        dataset.update(params)
         | 
| 28 | 
            +
                        dataset.first
         | 
| 29 | 
            +
                      end
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                    if dataset.respond_to?(:supports_insert_conflict?) && dataset.supports_insert_conflict?
         | 
| 33 | 
            +
                      def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, exclude_on_update = nil)
         | 
| 34 | 
            +
                        to_update = params.keys - unique_columns
         | 
| 35 | 
            +
                        to_update -= exclude_on_update if exclude_on_update
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                        dataset = dataset.insert_conflict(
         | 
| 38 | 
            +
                          target: unique_columns,
         | 
| 39 | 
            +
                          update: Hash[ to_update.map { |attribute| [attribute, Sequel[:excluded][attribute]] } ],
         | 
| 40 | 
            +
                          update_where: conds
         | 
| 41 | 
            +
                        )
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                        __insert_and_return__(dataset, pkey, params)
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
                    else
         | 
| 46 | 
            +
                      def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, exclude_on_update = nil)
         | 
| 47 | 
            +
                        find_params, update_params = params.partition { |key, _| unique_columns.include?(key) }.map { |h| Hash[h] }
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                        dataset_where = dataset.where(find_params)
         | 
| 50 | 
            +
                        record = if conds
         | 
| 51 | 
            +
                                   dataset_where_conds = dataset_where.where(conds)
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                                   # this means that there's still a valid entry there, so return early
         | 
| 54 | 
            +
                                   return if dataset_where.count != dataset_where_conds.count
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                                   dataset_where_conds.first
         | 
| 57 | 
            +
                                 else
         | 
| 58 | 
            +
                                   dataset_where.first
         | 
| 59 | 
            +
                                 end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                        if record
         | 
| 62 | 
            +
                          update_params.reject! { |k, _v| exclude_on_update.include?(k) } if exclude_on_update
         | 
| 63 | 
            +
                          __update_and_return__(dataset_where, update_params)
         | 
| 64 | 
            +
                        else
         | 
| 65 | 
            +
                          __insert_and_return__(dataset, pkey, params)
         | 
| 66 | 
            +
                        end
         | 
| 67 | 
            +
                      end
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
                # rubocop:enable Naming/MethodName, Metrics/ParameterLists
         | 
| 72 | 
            +
              end
         | 
| 73 | 
            +
            end
         | 
    
        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.2.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Tiago Cardoso
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2020- | 
| 11 | 
            +
            date: 2020-09-09 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:
         | 
| @@ -32,8 +32,10 @@ files: | |
| 32 32 | 
             
            - lib/rodauth/features/oauth.rb
         | 
| 33 33 | 
             
            - lib/rodauth/features/oauth_http_mac.rb
         | 
| 34 34 | 
             
            - lib/rodauth/features/oauth_jwt.rb
         | 
| 35 | 
            +
            - lib/rodauth/features/oauth_saml.rb
         | 
| 35 36 | 
             
            - lib/rodauth/features/oidc.rb
         | 
| 36 37 | 
             
            - lib/rodauth/oauth.rb
         | 
| 38 | 
            +
            - lib/rodauth/oauth/database_extensions.rb
         | 
| 37 39 | 
             
            - lib/rodauth/oauth/railtie.rb
         | 
| 38 40 | 
             
            - lib/rodauth/oauth/ttl_store.rb
         | 
| 39 41 | 
             
            - lib/rodauth/oauth/version.rb
         |