rodauth-oauth 0.7.4 → 0.8.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 +1 -424
- data/README.md +26 -389
- data/doc/release_notes/0_0_1.md +3 -0
- data/doc/release_notes/0_0_2.md +15 -0
- data/doc/release_notes/0_0_3.md +31 -0
- data/doc/release_notes/0_0_4.md +36 -0
- data/doc/release_notes/0_0_5.md +36 -0
- data/doc/release_notes/0_0_6.md +21 -0
- data/doc/release_notes/0_1_0.md +44 -0
- data/doc/release_notes/0_2_0.md +43 -0
- data/doc/release_notes/0_3_0.md +28 -0
- data/doc/release_notes/0_4_0.md +18 -0
- data/doc/release_notes/0_4_1.md +9 -0
- data/doc/release_notes/0_4_2.md +5 -0
- data/doc/release_notes/0_4_3.md +3 -0
- data/doc/release_notes/0_5_0.md +11 -0
- data/doc/release_notes/0_5_1.md +13 -0
- data/doc/release_notes/0_6_0.md +9 -0
- data/doc/release_notes/0_6_1.md +6 -0
- data/doc/release_notes/0_7_0.md +20 -0
- data/doc/release_notes/0_7_1.md +10 -0
- data/doc/release_notes/0_7_2.md +21 -0
- data/doc/release_notes/0_7_3.md +10 -0
- data/doc/release_notes/0_7_4.md +5 -0
- data/doc/release_notes/0_8_0.md +37 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +3 -3
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +11 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +20 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +22 -10
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +11 -5
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +38 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +5 -5
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +11 -15
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +9 -1
- data/lib/rodauth/features/oauth.rb +3 -1418
- data/lib/rodauth/features/oauth_application_management.rb +209 -0
- data/lib/rodauth/features/oauth_assertion_base.rb +96 -0
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +249 -0
- data/lib/rodauth/features/oauth_authorization_server.rb +0 -0
- data/lib/rodauth/features/oauth_base.rb +735 -0
- data/lib/rodauth/features/oauth_device_grant.rb +221 -0
- data/lib/rodauth/features/oauth_http_mac.rb +3 -21
- data/lib/rodauth/features/oauth_implicit_grant.rb +59 -0
- data/lib/rodauth/features/oauth_jwt.rb +37 -60
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +59 -0
- data/lib/rodauth/features/oauth_pkce.rb +98 -0
- data/lib/rodauth/features/oauth_resource_server.rb +21 -0
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +102 -0
- data/lib/rodauth/features/oauth_token_introspection.rb +108 -0
- data/lib/rodauth/features/oauth_token_management.rb +77 -0
- data/lib/rodauth/features/oauth_token_revocation.rb +109 -0
- data/lib/rodauth/features/oidc.rb +4 -3
- data/lib/rodauth/oauth/database_extensions.rb +15 -2
- data/lib/rodauth/oauth/refinements.rb +48 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +28 -12
- data/templates/authorize.str +7 -7
- data/templates/client_secret_field.str +2 -2
- data/templates/description_field.str +1 -1
- data/templates/device_search.str +11 -0
- data/templates/device_verification.str +24 -0
- data/templates/homepage_url_field.str +2 -2
- data/templates/jws_jwk_field.str +4 -0
- data/templates/jwt_public_key_field.str +4 -0
- data/templates/name_field.str +1 -1
- data/templates/new_oauth_application.str +9 -0
- data/templates/oauth_application.str +7 -3
- data/templates/oauth_application_oauth_tokens.str +51 -0
- data/templates/oauth_applications.str +2 -2
- data/templates/oauth_tokens.str +9 -11
- data/templates/redirect_uri_field.str +2 -2
- metadata +71 -3
- data/lib/rodauth/features/oauth_saml.rb +0 -104
| @@ -0,0 +1,209 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Rodauth
         | 
| 4 | 
            +
              Feature.define(:oauth_application_management, :OauthApplicationManagement) do
         | 
| 5 | 
            +
                depends :oauth_base
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                before "create_oauth_application"
         | 
| 8 | 
            +
                after "create_oauth_application"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                error_flash "There was an error registering your oauth application", "create_oauth_application"
         | 
| 11 | 
            +
                notice_flash "Your oauth application has been registered", "create_oauth_application"
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                view "oauth_applications", "Oauth Applications", "oauth_applications"
         | 
| 14 | 
            +
                view "oauth_application", "Oauth Application", "oauth_application"
         | 
| 15 | 
            +
                view "new_oauth_application", "New Oauth Application", "new_oauth_application"
         | 
| 16 | 
            +
                view "oauth_application_oauth_tokens", "Oauth Application Tokens", "oauth_application_oauth_tokens"
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                auth_value_method :oauth_valid_uri_schemes, %w[https]
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                # Application
         | 
| 21 | 
            +
                APPLICATION_REQUIRED_PARAMS = %w[name description scopes homepage_url redirect_uri client_secret].freeze
         | 
| 22 | 
            +
                auth_value_method :oauth_application_required_params, APPLICATION_REQUIRED_PARAMS
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                (APPLICATION_REQUIRED_PARAMS + %w[client_id]).each do |param|
         | 
| 25 | 
            +
                  auth_value_method :"oauth_application_#{param}_param", param
         | 
| 26 | 
            +
                  configuration_module_eval do
         | 
| 27 | 
            +
                    define_method :"#{param}_label" do
         | 
| 28 | 
            +
                      warn "#{__method__} is deprecated, switch to oauth_applications_#{__method__}_label"
         | 
| 29 | 
            +
                      __send__(:"oauth_applications_#{param}_label")
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                translatable_method :oauth_applications_name_label, "Name"
         | 
| 35 | 
            +
                translatable_method :oauth_applications_description_label, "Description"
         | 
| 36 | 
            +
                translatable_method :oauth_applications_scopes_label, "Scopes"
         | 
| 37 | 
            +
                translatable_method :oauth_applications_homepage_url_label, "Homepage URL"
         | 
| 38 | 
            +
                translatable_method :oauth_applications_redirect_uri_label, "Redirect URI"
         | 
| 39 | 
            +
                translatable_method :oauth_applications_client_secret_label, "Client Secret"
         | 
| 40 | 
            +
                translatable_method :oauth_applications_client_id_label, "Client ID"
         | 
| 41 | 
            +
                button "Register", "oauth_application"
         | 
| 42 | 
            +
                button "Revoke", "oauth_token_revoke"
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                auth_value_method :oauth_applications_oauth_tokens_path, "oauth-tokens"
         | 
| 45 | 
            +
                auth_value_method :oauth_applications_route, "oauth-applications"
         | 
| 46 | 
            +
                auth_value_method :oauth_applications_id_pattern, Integer
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                translatable_method :invalid_url_message, "Invalid URL"
         | 
| 49 | 
            +
                translatable_method :null_error_message, "is not filled"
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def oauth_applications_path(opts = {})
         | 
| 52 | 
            +
                  route_path(oauth_applications_route, opts)
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def oauth_applications_url(opts = {})
         | 
| 56 | 
            +
                  route_url(oauth_applications_route, opts)
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
                auth_value_methods(
         | 
| 59 | 
            +
                  :oauth_application_path
         | 
| 60 | 
            +
                )
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def oauth_application_path(id)
         | 
| 63 | 
            +
                  "#{oauth_applications_path}/#{id}"
         | 
| 64 | 
            +
                end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                # /oauth-applications routes
         | 
| 67 | 
            +
                def oauth_applications
         | 
| 68 | 
            +
                  request.on(oauth_applications_route) do
         | 
| 69 | 
            +
                    require_account
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    request.get "new" do
         | 
| 72 | 
            +
                      new_oauth_application_view
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    request.on(oauth_applications_id_pattern) do |id|
         | 
| 76 | 
            +
                      oauth_application = db[oauth_applications_table]
         | 
| 77 | 
            +
                                          .where(oauth_applications_id_column => id)
         | 
| 78 | 
            +
                                          .where(oauth_applications_account_id_column => account_id)
         | 
| 79 | 
            +
                                          .first
         | 
| 80 | 
            +
                      next unless oauth_application
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                      scope.instance_variable_set(:@oauth_application, oauth_application)
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                      request.is do
         | 
| 85 | 
            +
                        request.get do
         | 
| 86 | 
            +
                          oauth_application_view
         | 
| 87 | 
            +
                        end
         | 
| 88 | 
            +
                      end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                      request.on(oauth_applications_oauth_tokens_path) do
         | 
| 91 | 
            +
                        oauth_tokens = db[oauth_tokens_table].where(oauth_tokens_oauth_application_id_column => id)
         | 
| 92 | 
            +
                        scope.instance_variable_set(:@oauth_tokens, oauth_tokens)
         | 
| 93 | 
            +
                        request.get do
         | 
| 94 | 
            +
                          oauth_application_oauth_tokens_view
         | 
| 95 | 
            +
                        end
         | 
| 96 | 
            +
                      end
         | 
| 97 | 
            +
                    end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    request.get do
         | 
| 100 | 
            +
                      scope.instance_variable_set(:@oauth_applications, db[oauth_applications_table]
         | 
| 101 | 
            +
                        .where(oauth_applications_account_id_column => account_id))
         | 
| 102 | 
            +
                      oauth_applications_view
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                    request.post do
         | 
| 106 | 
            +
                      catch_error do
         | 
| 107 | 
            +
                        validate_oauth_application_params
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                        transaction do
         | 
| 110 | 
            +
                          before_create_oauth_application
         | 
| 111 | 
            +
                          id = create_oauth_application
         | 
| 112 | 
            +
                          after_create_oauth_application
         | 
| 113 | 
            +
                          set_notice_flash create_oauth_application_notice_flash
         | 
| 114 | 
            +
                          redirect "#{request.path}/#{id}"
         | 
| 115 | 
            +
                        end
         | 
| 116 | 
            +
                      end
         | 
| 117 | 
            +
                      set_error_flash create_oauth_application_error_flash
         | 
| 118 | 
            +
                      new_oauth_application_view
         | 
| 119 | 
            +
                    end
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                def check_csrf?
         | 
| 124 | 
            +
                  case request.path
         | 
| 125 | 
            +
                  when oauth_applications_path
         | 
| 126 | 
            +
                    only_json? ? false : super
         | 
| 127 | 
            +
                  else
         | 
| 128 | 
            +
                    super
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                private
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                def oauth_application_params
         | 
| 135 | 
            +
                  @oauth_application_params ||= oauth_application_required_params.each_with_object({}) do |param, params|
         | 
| 136 | 
            +
                    value = request.params[__send__(:"oauth_application_#{param}_param")]
         | 
| 137 | 
            +
                    if value && !value.empty?
         | 
| 138 | 
            +
                      params[param] = value
         | 
| 139 | 
            +
                    else
         | 
| 140 | 
            +
                      set_field_error(param, null_error_message)
         | 
| 141 | 
            +
                    end
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                def validate_oauth_application_params
         | 
| 146 | 
            +
                  oauth_application_params.each do |key, value|
         | 
| 147 | 
            +
                    if key == oauth_application_homepage_url_param
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                      set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                    elsif key == oauth_application_redirect_uri_param
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                      if value.respond_to?(:each)
         | 
| 154 | 
            +
                        value.each do |uri|
         | 
| 155 | 
            +
                          next if uri.empty?
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                          set_field_error(key, invalid_url_message) unless check_valid_uri?(uri)
         | 
| 158 | 
            +
                        end
         | 
| 159 | 
            +
                      else
         | 
| 160 | 
            +
                        set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
         | 
| 161 | 
            +
                      end
         | 
| 162 | 
            +
                    elsif key == oauth_application_scopes_param
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                      value.each do |scope|
         | 
| 165 | 
            +
                        set_field_error(key, invalid_scope_message) unless oauth_application_scopes.include?(scope)
         | 
| 166 | 
            +
                      end
         | 
| 167 | 
            +
                    end
         | 
| 168 | 
            +
                  end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                  throw :rodauth_error if @field_errors && !@field_errors.empty?
         | 
| 171 | 
            +
                end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                def create_oauth_application
         | 
| 174 | 
            +
                  create_params = {
         | 
| 175 | 
            +
                    oauth_applications_account_id_column => account_id,
         | 
| 176 | 
            +
                    oauth_applications_name_column => oauth_application_params[oauth_application_name_param],
         | 
| 177 | 
            +
                    oauth_applications_description_column => oauth_application_params[oauth_application_description_param],
         | 
| 178 | 
            +
                    oauth_applications_scopes_column => oauth_application_params[oauth_application_scopes_param],
         | 
| 179 | 
            +
                    oauth_applications_homepage_url_column => oauth_application_params[oauth_application_homepage_url_param]
         | 
| 180 | 
            +
                  }
         | 
| 181 | 
            +
             | 
| 182 | 
            +
                  redirect_uris = oauth_application_params[oauth_application_redirect_uri_param]
         | 
| 183 | 
            +
                  redirect_uris = redirect_uris.to_a.reject(&:empty?).join(" ") if redirect_uris.respond_to?(:each)
         | 
| 184 | 
            +
                  create_params[oauth_applications_redirect_uri_column] = redirect_uris unless redirect_uris.empty?
         | 
| 185 | 
            +
                  # set client ID/secret pairs
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                  create_params.merge! \
         | 
| 188 | 
            +
                    oauth_applications_client_secret_column => \
         | 
| 189 | 
            +
                      secret_hash(oauth_application_params[oauth_application_client_secret_param])
         | 
| 190 | 
            +
             | 
| 191 | 
            +
                  create_params[oauth_applications_scopes_column] = if create_params[oauth_applications_scopes_column]
         | 
| 192 | 
            +
                                                                      create_params[oauth_applications_scopes_column].join(oauth_scope_separator)
         | 
| 193 | 
            +
                                                                    else
         | 
| 194 | 
            +
                                                                      oauth_application_default_scope
         | 
| 195 | 
            +
                                                                    end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                  rescue_from_uniqueness_error do
         | 
| 198 | 
            +
                    create_params[oauth_applications_client_id_column] = oauth_unique_id_generator
         | 
| 199 | 
            +
                    db[oauth_applications_table].insert(create_params)
         | 
| 200 | 
            +
                  end
         | 
| 201 | 
            +
                end
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                def oauth_server_metadata_body(*)
         | 
| 204 | 
            +
                  super.tap do |data|
         | 
| 205 | 
            +
                    data[:registration_endpoint] = oauth_applications_url
         | 
| 206 | 
            +
                  end
         | 
| 207 | 
            +
                end
         | 
| 208 | 
            +
              end
         | 
| 209 | 
            +
            end
         | 
| @@ -0,0 +1,96 @@ | |
| 1 | 
            +
            # frozen-string-literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "rodauth/oauth/refinements"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Rodauth
         | 
| 6 | 
            +
              Feature.define(:oauth_assertion_base, :OauthAssertionBase) do
         | 
| 7 | 
            +
                using PrefixExtensions
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                depends :oauth_base
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                auth_value_methods(
         | 
| 12 | 
            +
                  :assertion_grant_type?,
         | 
| 13 | 
            +
                  :client_assertion_type?,
         | 
| 14 | 
            +
                  :assertion_grant_type,
         | 
| 15 | 
            +
                  :client_assertion_type
         | 
| 16 | 
            +
                )
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                private
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def validate_oauth_token_params
         | 
| 21 | 
            +
                  return super unless assertion_grant_type?
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  redirect_response_error("invalid_grant") unless param_or_nil("assertion")
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def require_oauth_application
         | 
| 27 | 
            +
                  if assertion_grant_type?
         | 
| 28 | 
            +
                    @oauth_application = __send__(:"require_oauth_application_from_#{assertion_grant_type}_assertion_issuer", param("assertion"))
         | 
| 29 | 
            +
                  elsif client_assertion_type?
         | 
| 30 | 
            +
                    @oauth_application = __send__(:"require_oauth_application_from_#{client_assertion_type}_assertion_subject",
         | 
| 31 | 
            +
                                                  param("client_assertion"))
         | 
| 32 | 
            +
                  else
         | 
| 33 | 
            +
                    return super
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  redirect_response_error("invalid_grant") unless @oauth_application
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  if client_assertion_type? &&
         | 
| 39 | 
            +
                     (client_id = param_or_nil("client_id")) &&
         | 
| 40 | 
            +
                     client_id != @oauth_application[oauth_applications_client_id_column]
         | 
| 41 | 
            +
                    # If present, the value of the
         | 
| 42 | 
            +
                    # "client_id" parameter MUST identify the same client as is
         | 
| 43 | 
            +
                    # identified by the client assertion.
         | 
| 44 | 
            +
                    redirect_response_error("invalid_grant")
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def account_from_bearer_assertion_subject(subject)
         | 
| 49 | 
            +
                  __insert_or_do_nothing_and_return__(
         | 
| 50 | 
            +
                    db[accounts_table],
         | 
| 51 | 
            +
                    account_id_column,
         | 
| 52 | 
            +
                    [login_column],
         | 
| 53 | 
            +
                    login_column => subject
         | 
| 54 | 
            +
                  )
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def create_oauth_token(grant_type)
         | 
| 58 | 
            +
                  return super unless assertion_grant_type?(grant_type)
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  account = __send__(:"account_from_#{assertion_grant_type}_assertion", param("assertion"))
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  redirect_response_error("invalid_grant") unless account
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  grant_scopes = if param_or_nil("scope")
         | 
| 65 | 
            +
                                   redirect_response_error("invalid_grant") unless check_valid_scopes?
         | 
| 66 | 
            +
                                   scopes
         | 
| 67 | 
            +
                                 else
         | 
| 68 | 
            +
                                   @oauth_application[oauth_applications_scopes_column]
         | 
| 69 | 
            +
                                 end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  create_params = {
         | 
| 72 | 
            +
                    oauth_tokens_account_id_column => account[account_id_column],
         | 
| 73 | 
            +
                    oauth_tokens_oauth_application_id_column => @oauth_application[oauth_applications_id_column],
         | 
| 74 | 
            +
                    oauth_tokens_scopes_column => grant_scopes
         | 
| 75 | 
            +
                  }
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  generate_oauth_token(create_params, false)
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def assertion_grant_type?(grant_type = param("grant_type"))
         | 
| 81 | 
            +
                  grant_type.start_with?("urn:ietf:params:oauth:grant-type:")
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def client_assertion_type?(client_assertion_type = param("client_assertion_type"))
         | 
| 85 | 
            +
                  client_assertion_type.start_with?("urn:ietf:params:oauth:client-assertion-type:")
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def assertion_grant_type(grant_type = param("grant_type"))
         | 
| 89 | 
            +
                  grant_type.delete_prefix("urn:ietf:params:oauth:grant-type:").tr("-", "_")
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def client_assertion_type(assertion_type = param("client_assertion_type"))
         | 
| 93 | 
            +
                  assertion_type.delete_prefix("urn:ietf:params:oauth:client-assertion-type:").tr("-", "_")
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
              end
         | 
| 96 | 
            +
            end
         | 
| @@ -0,0 +1,249 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Rodauth
         | 
| 4 | 
            +
              Feature.define(:oauth_authorization_code_grant, :OauthAuthorizationCodeGrant) do
         | 
| 5 | 
            +
                depends :oauth_base
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                before "authorize"
         | 
| 8 | 
            +
                after "authorize"
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                view "authorize", "Authorize", "authorize"
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                button "Authorize", "oauth_authorize"
         | 
| 13 | 
            +
                button "Back to Client Application", "oauth_authorize_post"
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                auth_value_method :use_oauth_access_type?, true
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                # OAuth Grants
         | 
| 18 | 
            +
                auth_value_method :oauth_grants_table, :oauth_grants
         | 
| 19 | 
            +
                auth_value_method :oauth_grants_id_column, :id
         | 
| 20 | 
            +
                %i[
         | 
| 21 | 
            +
                  account_id oauth_application_id
         | 
| 22 | 
            +
                  redirect_uri code scopes access_type
         | 
| 23 | 
            +
                  expires_in revoked_at
         | 
| 24 | 
            +
                ].each do |column|
         | 
| 25 | 
            +
                  auth_value_method :"oauth_grants_#{column}_column", column
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                translatable_method :oauth_tokens_scopes_label, "Scopes"
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                # /authorize
         | 
| 31 | 
            +
                route(:authorize) do |r|
         | 
| 32 | 
            +
                  next unless is_authorization_server?
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  before_authorize_route
         | 
| 35 | 
            +
                  require_authorizable_account
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  validate_oauth_grant_params
         | 
| 38 | 
            +
                  try_approval_prompt if use_oauth_access_type? && request.get?
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  r.get do
         | 
| 41 | 
            +
                    authorize_view
         | 
| 42 | 
            +
                  end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  r.post do
         | 
| 45 | 
            +
                    params, mode = transaction do
         | 
| 46 | 
            +
                      before_authorize
         | 
| 47 | 
            +
                      do_authorize
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    authorize_response(params, mode)
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                def check_csrf?
         | 
| 55 | 
            +
                  case request.path
         | 
| 56 | 
            +
                  when authorize_path
         | 
| 57 | 
            +
                    only_json? ? false : super
         | 
| 58 | 
            +
                  else
         | 
| 59 | 
            +
                    super
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                private
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def validate_oauth_grant_params
         | 
| 66 | 
            +
                  redirect_response_error("invalid_request", request.referer || default_redirect) unless oauth_application && check_valid_redirect_uri?
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  unless oauth_application && check_valid_redirect_uri? && check_valid_access_type? &&
         | 
| 69 | 
            +
                         check_valid_approval_prompt? && check_valid_response_type?
         | 
| 70 | 
            +
                    redirect_response_error("invalid_request")
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                  redirect_response_error("invalid_scope") unless check_valid_scopes?
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  return unless (response_mode = param_or_nil("response_mode")) && response_mode != "form_post"
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                  redirect_response_error("invalid_request")
         | 
| 77 | 
            +
                end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                def validate_oauth_token_params
         | 
| 80 | 
            +
                  redirect_response_error("invalid_request") if param_or_nil("grant_type") == "authorization_code" && !param_or_nil("code")
         | 
| 81 | 
            +
                  super
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def try_approval_prompt
         | 
| 85 | 
            +
                  approval_prompt = param_or_nil("approval_prompt")
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  return unless approval_prompt && approval_prompt == "auto"
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  return if db[oauth_grants_table].where(
         | 
| 90 | 
            +
                    oauth_grants_account_id_column => account_id,
         | 
| 91 | 
            +
                    oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 92 | 
            +
                    oauth_grants_redirect_uri_column => redirect_uri,
         | 
| 93 | 
            +
                    oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
         | 
| 94 | 
            +
                    oauth_grants_access_type_column => "online"
         | 
| 95 | 
            +
                  ).count.zero?
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  # if there's a previous oauth grant for the params combo, it means that this user has approved before.
         | 
| 98 | 
            +
                  request.env["REQUEST_METHOD"] = "POST"
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def create_oauth_grant(create_params = {})
         | 
| 102 | 
            +
                  create_params.merge!(
         | 
| 103 | 
            +
                    oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 104 | 
            +
                    oauth_grants_redirect_uri_column => redirect_uri,
         | 
| 105 | 
            +
                    oauth_grants_expires_in_column => Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in),
         | 
| 106 | 
            +
                    oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
         | 
| 107 | 
            +
                  )
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                  # Access Type flow
         | 
| 110 | 
            +
                  if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
         | 
| 111 | 
            +
                    create_params[oauth_grants_access_type_column] = access_type
         | 
| 112 | 
            +
                  end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                  ds = db[oauth_grants_table]
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                  rescue_from_uniqueness_error do
         | 
| 117 | 
            +
                    create_params[oauth_grants_code_column] = oauth_unique_id_generator
         | 
| 118 | 
            +
                    __insert_and_return__(ds, oauth_grants_id_column, create_params)
         | 
| 119 | 
            +
                  end
         | 
| 120 | 
            +
                  create_params[oauth_grants_code_column]
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
         | 
| 124 | 
            +
                  case param("response_type")
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  when "code"
         | 
| 127 | 
            +
                    response_mode ||= "query"
         | 
| 128 | 
            +
                    response_params.replace(_do_authorize_code)
         | 
| 129 | 
            +
                  when "none"
         | 
| 130 | 
            +
                    response_mode ||= "none"
         | 
| 131 | 
            +
                  when "", nil
         | 
| 132 | 
            +
                    response_mode ||= oauth_response_mode
         | 
| 133 | 
            +
                    response_params.replace(_do_authorize_code)
         | 
| 134 | 
            +
                  end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                  response_params["state"] = param("state") if param_or_nil("state")
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                  [response_params, response_mode]
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
                def _do_authorize_code
         | 
| 142 | 
            +
                  { "code" => create_oauth_grant(oauth_grants_account_id_column => account_id) }
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                def authorize_response(params, mode)
         | 
| 146 | 
            +
                  redirect_url = URI.parse(redirect_uri)
         | 
| 147 | 
            +
                  case mode
         | 
| 148 | 
            +
                  when "query"
         | 
| 149 | 
            +
                    params = params.map { |k, v| "#{k}=#{v}" }
         | 
| 150 | 
            +
                    params << redirect_url.query if redirect_url.query
         | 
| 151 | 
            +
                    redirect_url.query = params.join("&")
         | 
| 152 | 
            +
                    redirect(redirect_url.to_s)
         | 
| 153 | 
            +
                  when "form_post"
         | 
| 154 | 
            +
                    scope.view layout: false, inline: <<-FORM
         | 
| 155 | 
            +
                      <html>
         | 
| 156 | 
            +
                        <head><title>Authorized</title></head>
         | 
| 157 | 
            +
                        <body onload="javascript:document.forms[0].submit()">
         | 
| 158 | 
            +
                          <form method="post" action="#{redirect_uri}">
         | 
| 159 | 
            +
                            #{
         | 
| 160 | 
            +
                              params.map do |name, value|
         | 
| 161 | 
            +
                                "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
         | 
| 162 | 
            +
                              end.join
         | 
| 163 | 
            +
                            }
         | 
| 164 | 
            +
                            <input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
         | 
| 165 | 
            +
                          </form>
         | 
| 166 | 
            +
                        </body>
         | 
| 167 | 
            +
                      </html>
         | 
| 168 | 
            +
                    FORM
         | 
| 169 | 
            +
                  when "none"
         | 
| 170 | 
            +
                    redirect(redirect_url.to_s)
         | 
| 171 | 
            +
                  end
         | 
| 172 | 
            +
                end
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                def create_oauth_token(grant_type)
         | 
| 175 | 
            +
                  return super unless grant_type == "authorization_code"
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                  # fetch oauth grant
         | 
| 178 | 
            +
                  oauth_grant = db[oauth_grants_table].where(
         | 
| 179 | 
            +
                    oauth_grants_code_column => param("code"),
         | 
| 180 | 
            +
                    oauth_grants_redirect_uri_column => param("redirect_uri"),
         | 
| 181 | 
            +
                    oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 182 | 
            +
                    oauth_grants_revoked_at_column => nil
         | 
| 183 | 
            +
                  ).where(Sequel[oauth_grants_expires_in_column] >= Sequel::CURRENT_TIMESTAMP)
         | 
| 184 | 
            +
                                                      .for_update
         | 
| 185 | 
            +
                                                      .first
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                  redirect_response_error("invalid_grant") unless oauth_grant
         | 
| 188 | 
            +
             | 
| 189 | 
            +
                  create_params = {
         | 
| 190 | 
            +
                    oauth_tokens_account_id_column => oauth_grant[oauth_grants_account_id_column],
         | 
| 191 | 
            +
                    oauth_tokens_oauth_application_id_column => oauth_grant[oauth_grants_oauth_application_id_column],
         | 
| 192 | 
            +
                    oauth_tokens_oauth_grant_id_column => oauth_grant[oauth_grants_id_column],
         | 
| 193 | 
            +
                    oauth_tokens_scopes_column => oauth_grant[oauth_grants_scopes_column]
         | 
| 194 | 
            +
                  }
         | 
| 195 | 
            +
                  create_oauth_token_from_authorization_code(oauth_grant, create_params)
         | 
| 196 | 
            +
                end
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                def create_oauth_token_from_authorization_code(oauth_grant, create_params)
         | 
| 199 | 
            +
                  # revoke oauth grant
         | 
| 200 | 
            +
                  db[oauth_grants_table].where(oauth_grants_id_column => oauth_grant[oauth_grants_id_column])
         | 
| 201 | 
            +
                                        .update(oauth_grants_revoked_at_column => Sequel::CURRENT_TIMESTAMP)
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                  should_generate_refresh_token = !use_oauth_access_type? ||
         | 
| 204 | 
            +
                                                  oauth_grant[oauth_grants_access_type_column] == "offline"
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                  generate_oauth_token(create_params, should_generate_refresh_token)
         | 
| 207 | 
            +
                end
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                ACCESS_TYPES = %w[offline online].freeze
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                def check_valid_access_type?
         | 
| 212 | 
            +
                  return true unless use_oauth_access_type?
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                  access_type = param_or_nil("access_type")
         | 
| 215 | 
            +
                  !access_type || ACCESS_TYPES.include?(access_type)
         | 
| 216 | 
            +
                end
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                APPROVAL_PROMPTS = %w[force auto].freeze
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                def check_valid_approval_prompt?
         | 
| 221 | 
            +
                  return true unless use_oauth_access_type?
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                  approval_prompt = param_or_nil("approval_prompt")
         | 
| 224 | 
            +
                  !approval_prompt || APPROVAL_PROMPTS.include?(approval_prompt)
         | 
| 225 | 
            +
                end
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                def check_valid_response_type?
         | 
| 228 | 
            +
                  response_type = param_or_nil("response_type")
         | 
| 229 | 
            +
             | 
| 230 | 
            +
                  response_type.nil? || response_type == "code"
         | 
| 231 | 
            +
                end
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                def check_valid_redirect_uri?
         | 
| 234 | 
            +
                  oauth_application[oauth_applications_redirect_uri_column].split(" ").include?(redirect_uri)
         | 
| 235 | 
            +
                end
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                def oauth_server_metadata_body(*)
         | 
| 238 | 
            +
                  super.tap do |data|
         | 
| 239 | 
            +
                    data[:authorization_endpoint] = authorize_url
         | 
| 240 | 
            +
                    data[:response_types_supported] << "code"
         | 
| 241 | 
            +
             | 
| 242 | 
            +
                    data[:response_modes_supported] << "query"
         | 
| 243 | 
            +
                    data[:response_modes_supported] << "form_post"
         | 
| 244 | 
            +
             | 
| 245 | 
            +
                    data[:grant_types_supported] << "authorization_code"
         | 
| 246 | 
            +
                  end
         | 
| 247 | 
            +
                end
         | 
| 248 | 
            +
              end
         | 
| 249 | 
            +
            end
         | 
| 
            File without changes
         |