rodauth-oauth 1.2.0 → 1.3.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/README.md +2 -0
- data/doc/release_notes/1_1_0.md +1 -1
- data/doc/release_notes/1_3_0.md +38 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +3 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +11 -10
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +54 -32
- data/lib/rodauth/features/oauth_authorize_base.rb +8 -0
- data/lib/rodauth/features/oauth_base.rb +16 -16
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +7 -7
- data/lib/rodauth/features/oauth_implicit_grant.rb +22 -4
- data/lib/rodauth/features/oauth_jwt_secured_authorization_response_mode.rb +126 -0
- data/lib/rodauth/features/oauth_management_base.rb +1 -3
- data/lib/rodauth/features/oidc.rb +87 -50
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +39 -1
- data/lib/rodauth/features/oidc_self_issued.rb +73 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/templates/authorize.str +1 -0
- metadata +6 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 4d7d5f8b68686703954bf4e335cef0ea33f9e31c94c439df84f08e8ff3270829
         | 
| 4 | 
            +
              data.tar.gz: 1da57ba2082818a74dbca4d1c6bcab0c15f97da891e12c03a8bf91440a4edcfd
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 8230b54e51d2081e25d1386d6294745d54eebbe11a6677bdb9cade14e0a418658bc2b8a67ae2e6355458f4b43d8a2df1700cd3e0496fa8a10e690318f3d03ba0
         | 
| 7 | 
            +
              data.tar.gz: 31ab5721a6464b751860b6896f47999e189592582842ba419ab0a057ff38af98612d54a8b00177092e2fe5993af1e5554cecafbbfaab18a495656117f19ce4fd
         | 
    
        data/README.md
    CHANGED
    
    | @@ -17,6 +17,7 @@ This is an extension to the `rodauth` gem which implements the [OAuth 2.0 framew | |
| 17 17 | 
             
            * Config OP
         | 
| 18 18 | 
             
            * Dynamic OP
         | 
| 19 19 | 
             
            * Form Post OP
         | 
| 20 | 
            +
            * 3rd Party-Init OP
         | 
| 20 21 |  | 
| 21 22 | 
             
            (it also passes the conformance tests for the RP-Initiated Logout OP).
         | 
| 22 23 |  | 
| @@ -39,6 +40,7 @@ This gem implements the following RFCs and features of OAuth: | |
| 39 40 | 
             
              * `oauth_tls_client_auth` - [Mutual-TLS Client Authentication](https://datatracker.ietf.org/doc/html/rfc8705);
         | 
| 40 41 | 
             
              * `oauth_jwt` - [JWT Access Tokens](https://tools.ietf.org/html/draft-ietf-oauth-access-token-jwt-07);
         | 
| 41 42 | 
             
              * `oauth_jwt_secured_authorization_request` - [JWT Secured Authorization Request](https://tools.ietf.org/html/draft-ietf-oauth-jwsreq-20);
         | 
| 43 | 
            +
              * `oauth_jwt_secured_authorization_response_mode` - [JWT Secured Authorization Response_mode](https://openid.net/specs/openid-financial-api-jarm.html);
         | 
| 42 44 | 
             
              * `oauth_resource_indicators` - [Resource Indicators](https://datatracker.ietf.org/doc/html/rfc8707);
         | 
| 43 45 | 
             
              * Access Type (Token refresh online and offline);
         | 
| 44 46 | 
             
            * `oauth_http_mac` - [MAC Authentication Scheme](https://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-02);
         | 
    
        data/doc/release_notes/1_1_0.md
    CHANGED
    
    
| @@ -0,0 +1,38 @@ | |
| 1 | 
            +
            ## 1.3.0 (02/04/2023)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            ## Features
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ### Self-Signed Issued Tokens
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            `rodauth-oauth` supports self-signed issued tokens, via the `oidc_self_issued` feature.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            More info about the feature [in the docs](https://gitlab.com/os85/rodauth-oauth/-/wikis/Self-Issued-OpenID).
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            #### JARM
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            `rodauth-oauth` supports JWT-secured Authorization Response Mode, also known as JARM, via the `oauth_jwt_secured_authorization_response_mode`.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            More info about the feature [in the docs](https://gitlab.com/os85/rodauth-oauth/-/wikis/JWT-Secured-Authorization-Response-Mode).
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            ## Improvements
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            ### `fill_with_account_claims` auth method
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            `fill_with_account_claims` is now exposed as an auth method. This allows one to override to be able to cover certain requirements, such as aggregated and distributed claims. Here's a [link to the docs](https://gitlab.com/os85/rodauth-oauth/-/wikis/Id-Token-Authentication#claim-types) explaining how to do it.
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            ### oidc: only generate refresh token when `offline_access` scope is used.
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            When the `oidc` feature is used, refresh tokens won't be generated anymore by default; in order to do so, the `offline_access` needs to be requested for in the respective authorization request, [as the spec mandates](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess).
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            ### oidc: implicit grant loaded by default
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            The `oidc` feature now loads the `oauth_implicit_grant` feature by default. This hadn't been done before due to the wish to ship a secure integration by default, but since then, spec compliance became more prioritary, and this is a requirement.
         | 
| 30 | 
            +
             | 
| 31 | 
            +
            ## Bugfixes
         | 
| 32 | 
            +
             | 
| 33 | 
            +
            * rails integration: activerecord migrations fixes:
         | 
| 34 | 
            +
              * use `bigint` for foreign keys;
         | 
| 35 | 
            +
              * index creation instruction with the wrong syntax;
         | 
| 36 | 
            +
              * set precision 6 for default timestamps, to comply with AR defaults;
         | 
| 37 | 
            +
              * add missing `code` column to the `oauth_pushed_requests` table;
         | 
| 38 | 
            +
            * oidc: when using the `id_token` , or any composite response type including `id_token`, using any response mode other than `fragment` will result in an invalid request.
         | 
| @@ -75,6 +75,9 @@ | |
| 75 75 | 
             
                  <% if params[:acr_values] %>
         | 
| 76 76 | 
             
                    <%= hidden_field_tag :acr_values,  params[:acr_values] %>
         | 
| 77 77 | 
             
                  <% end %>
         | 
| 78 | 
            +
                  <% if params[:registration] %>
         | 
| 79 | 
            +
                    <%= hidden_field_tag :registration,  params[:registration] %>
         | 
| 80 | 
            +
                  <% end %>
         | 
| 78 81 | 
             
                <% end %>
         | 
| 79 82 | 
             
              </div>
         | 
| 80 83 | 
             
             <p class="text-center">
         | 
| @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
         | 
| 2 2 | 
             
              def change
         | 
| 3 3 | 
             
                create_table :oauth_applications do |t|
         | 
| 4 | 
            -
                  t. | 
| 4 | 
            +
                  t.bigint :account_id
         | 
| 5 5 | 
             
                  t.foreign_key :accounts, column: :account_id
         | 
| 6 6 | 
             
                  t.string :name, null: false
         | 
| 7 7 | 
             
                  t.string :description, null: true
         | 
| @@ -11,7 +11,7 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %> | |
| 11 11 | 
             
                  t.string :client_secret, null: false, index: { unique: true }
         | 
| 12 12 | 
             
                  t.string :registration_access_token, null: true
         | 
| 13 13 | 
             
                  t.string :scopes, null: false
         | 
| 14 | 
            -
                  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
         | 
| 14 | 
            +
                  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP(6)" }
         | 
| 15 15 |  | 
| 16 16 | 
             
                  # :oauth_dynamic_client_configuration enabled, extra optional params
         | 
| 17 17 | 
             
                  t.string :token_endpoint_auth_method, null: true
         | 
| @@ -61,20 +61,20 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %> | |
| 61 61 | 
             
                end
         | 
| 62 62 |  | 
| 63 63 | 
             
                create_table :oauth_grants do |t|
         | 
| 64 | 
            -
                  t. | 
| 64 | 
            +
                  t.bigint :account_id
         | 
| 65 65 | 
             
                  t.foreign_key :accounts, column: :account_id
         | 
| 66 | 
            -
                  t. | 
| 66 | 
            +
                  t.bigint :oauth_application_id
         | 
| 67 67 | 
             
                  t.foreign_key :oauth_applications, column: :oauth_application_id
         | 
| 68 68 | 
             
                  t.string :type, null: true
         | 
| 69 69 | 
             
                  t.string :code, null: true
         | 
| 70 70 | 
             
                  t.index(%i[oauth_application_id code], unique: true)
         | 
| 71 | 
            -
                  t.string :token, unique: true
         | 
| 72 | 
            -
                  t.string :refresh_token, unique: true
         | 
| 71 | 
            +
                  t.string :token, index: { unique: true }
         | 
| 72 | 
            +
                  t.string :refresh_token, index: { unique: true }
         | 
| 73 73 | 
             
                  t.datetime :expires_in, null: false
         | 
| 74 74 | 
             
                  t.string :redirect_uri
         | 
| 75 75 | 
             
                  t.datetime :revoked_at
         | 
| 76 76 | 
             
                  t.string :scopes, null: false
         | 
| 77 | 
            -
                  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
         | 
| 77 | 
            +
                  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP(6)" }
         | 
| 78 78 | 
             
                  t.string :access_type, null: false, default: "offline"
         | 
| 79 79 |  | 
| 80 80 | 
             
                  # :oauth_pkce enabled
         | 
| @@ -82,7 +82,7 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %> | |
| 82 82 | 
             
                  t.string :code_challenge_method
         | 
| 83 83 |  | 
| 84 84 | 
             
                  # :oauth_device_code_grant enabled
         | 
| 85 | 
            -
                  t.string :user_code, null: true, unique: true
         | 
| 85 | 
            +
                  t.string :user_code, null: true, index: { unique: true }
         | 
| 86 86 | 
             
                  t.datetime :last_polled_at, null: true
         | 
| 87 87 |  | 
| 88 88 | 
             
                  # :oauth_tls_client_auth
         | 
| @@ -99,11 +99,12 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %> | |
| 99 99 | 
             
                end
         | 
| 100 100 |  | 
| 101 101 | 
             
                create_table :oauth_pushed_requests do |t|
         | 
| 102 | 
            -
                  t. | 
| 102 | 
            +
                  t.bigint :oauth_application_id
         | 
| 103 103 | 
             
                  t.foreign_key :oauth_applications, column: :oauth_application_id
         | 
| 104 | 
            +
                  t.string :code, null: false, index: { unique: true }
         | 
| 104 105 | 
             
                  t.string :params, null: false
         | 
| 105 106 | 
             
                  t.datetime :expires_in, null: false
         | 
| 106 107 | 
             
                  t.index %i[oauth_application_id code], unique: true
         | 
| 107 108 | 
             
                end
         | 
| 108 109 | 
             
              end
         | 
| 109 | 
            -
            end
         | 
| 110 | 
            +
            end
         | 
| @@ -27,7 +27,19 @@ module Rodauth | |
| 27 27 |  | 
| 28 28 | 
             
                  response_mode = param_or_nil("response_mode")
         | 
| 29 29 |  | 
| 30 | 
            -
                   | 
| 30 | 
            +
                  return unless response_mode
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  redirect_response_error("invalid_request") unless oauth_response_modes_supported.include?(response_mode)
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  response_type = param_or_nil("response_type")
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  return unless response_type.nil? || response_type == "code"
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  redirect_response_error("invalid_request") unless oauth_response_modes_for_code_supported.include?(response_mode)
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def oauth_response_modes_for_code_supported
         | 
| 42 | 
            +
                  %w[query form_post]
         | 
| 31 43 | 
             
                end
         | 
| 32 44 |  | 
| 33 45 | 
             
                def validate_token_params
         | 
| @@ -67,55 +79,65 @@ module Rodauth | |
| 67 79 | 
             
                  redirect_url = URI.parse(redirect_uri)
         | 
| 68 80 | 
             
                  case mode
         | 
| 69 81 | 
             
                  when "query"
         | 
| 70 | 
            -
                    params =  | 
| 82 | 
            +
                    params = [URI.encode_www_form(params)]
         | 
| 71 83 | 
             
                    params << redirect_url.query if redirect_url.query
         | 
| 72 84 | 
             
                    redirect_url.query = params.join("&")
         | 
| 73 85 | 
             
                    redirect(redirect_url.to_s)
         | 
| 74 86 | 
             
                  when "form_post"
         | 
| 75 | 
            -
                     | 
| 76 | 
            -
                       | 
| 77 | 
            -
                        < | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 80 | 
            -
             | 
| 81 | 
            -
                              params.map do |name, value|
         | 
| 82 | 
            -
                                "<input type=\"hidden\" name=\"#{scope.h(name)}\" value=\"#{scope.h(value)}\" />"
         | 
| 83 | 
            -
                              end.join
         | 
| 84 | 
            -
                            }
         | 
| 85 | 
            -
                            <input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
         | 
| 86 | 
            -
                          </form>
         | 
| 87 | 
            -
                        </body>
         | 
| 88 | 
            -
                      </html>
         | 
| 89 | 
            -
                    FORM
         | 
| 87 | 
            +
                    inline_html = form_post_response_html(redirect_uri) do
         | 
| 88 | 
            +
                      params.map do |name, value|
         | 
| 89 | 
            +
                        "<input type=\"hidden\" name=\"#{scope.h(name)}\" value=\"#{scope.h(value)}\" />"
         | 
| 90 | 
            +
                      end.join
         | 
| 91 | 
            +
                    end
         | 
| 92 | 
            +
                    scope.view layout: false, inline: inline_html
         | 
| 90 93 | 
             
                  end
         | 
| 91 94 | 
             
                end
         | 
| 92 95 |  | 
| 93 | 
            -
                def _redirect_response_error(redirect_url,  | 
| 96 | 
            +
                def _redirect_response_error(redirect_url, params)
         | 
| 94 97 | 
             
                  response_mode = param_or_nil("response_mode") || oauth_response_mode
         | 
| 95 98 |  | 
| 96 99 | 
             
                  case response_mode
         | 
| 97 100 | 
             
                  when "form_post"
         | 
| 98 101 | 
             
                    response["Content-Type"] = "text/html"
         | 
| 99 | 
            -
                     | 
| 100 | 
            -
                       | 
| 101 | 
            -
                        < | 
| 102 | 
            -
             | 
| 103 | 
            -
             | 
| 104 | 
            -
             | 
| 105 | 
            -
                              query_params.map do |name, value|
         | 
| 106 | 
            -
                                "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
         | 
| 107 | 
            -
                              end.join
         | 
| 108 | 
            -
                            }
         | 
| 109 | 
            -
                          </form>
         | 
| 110 | 
            -
                        </body>
         | 
| 111 | 
            -
                      </html>
         | 
| 112 | 
            -
                    FORM
         | 
| 102 | 
            +
                    error_body = form_post_error_response_html(redirect_url) do
         | 
| 103 | 
            +
                      params.map do |name, value|
         | 
| 104 | 
            +
                        "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
         | 
| 105 | 
            +
                      end.join
         | 
| 106 | 
            +
                    end
         | 
| 107 | 
            +
                    response.write(error_body)
         | 
| 113 108 | 
             
                    request.halt
         | 
| 114 109 | 
             
                  else
         | 
| 115 110 | 
             
                    super
         | 
| 116 111 | 
             
                  end
         | 
| 117 112 | 
             
                end
         | 
| 118 113 |  | 
| 114 | 
            +
                def form_post_response_html(url)
         | 
| 115 | 
            +
                  <<-FORM
         | 
| 116 | 
            +
                    <html>
         | 
| 117 | 
            +
                      <head><title>Authorized</title></head>
         | 
| 118 | 
            +
                      <body onload="javascript:document.forms[0].submit()">
         | 
| 119 | 
            +
                        <form method="post" action="#{url}">
         | 
| 120 | 
            +
                          #{yield}
         | 
| 121 | 
            +
                          <input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
         | 
| 122 | 
            +
                        </form>
         | 
| 123 | 
            +
                      </body>
         | 
| 124 | 
            +
                    </html>
         | 
| 125 | 
            +
                  FORM
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                def form_post_error_response_html(url)
         | 
| 129 | 
            +
                  <<-FORM
         | 
| 130 | 
            +
                    <html>
         | 
| 131 | 
            +
                      <head><title></title></head>
         | 
| 132 | 
            +
                      <body onload="javascript:document.forms[0].submit()">
         | 
| 133 | 
            +
                        <form method="post" action="#{url}">
         | 
| 134 | 
            +
                          #{yield}
         | 
| 135 | 
            +
                        </form>
         | 
| 136 | 
            +
                      </body>
         | 
| 137 | 
            +
                    </html>
         | 
| 138 | 
            +
                  FORM
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
             | 
| 119 141 | 
             
                def create_token(grant_type)
         | 
| 120 142 | 
             
                  return super unless supported_grant_type?(grant_type, "authorization_code")
         | 
| 121 143 |  | 
| @@ -92,6 +92,14 @@ module Rodauth | |
| 92 92 | 
             
                  try_approval_prompt if use_oauth_access_type? && request.get?
         | 
| 93 93 |  | 
| 94 94 | 
             
                  redirect_response_error("invalid_scope") if (request.post? || param_or_nil("scope")) && !check_valid_scopes?
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  response_mode = param_or_nil("response_mode")
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  redirect_response_error("invalid_request") unless response_mode.nil? || oauth_response_modes_supported.include?(response_mode)
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def check_valid_scopes?(scp = scopes)
         | 
| 102 | 
            +
                  super(scp - %w[offline_access])
         | 
| 95 103 | 
             
                end
         | 
| 96 104 |  | 
| 97 105 | 
             
                def check_valid_response_type?
         | 
| @@ -762,31 +762,31 @@ module Rodauth | |
| 762 762 | 
             
                    throw_json_response_error(status_code, error_code)
         | 
| 763 763 | 
             
                  else
         | 
| 764 764 | 
             
                    redirect_url = URI.parse(redirect_url)
         | 
| 765 | 
            -
                     | 
| 765 | 
            +
                    params = []
         | 
| 766 766 |  | 
| 767 | 
            -
                     | 
| 768 | 
            -
             | 
| 769 | 
            -
             | 
| 770 | 
            -
             | 
| 771 | 
            -
             | 
| 767 | 
            +
                    params << if respond_to?(:"oauth_#{error_code}_error_code")
         | 
| 768 | 
            +
                                ["error", send(:"oauth_#{error_code}_error_code")]
         | 
| 769 | 
            +
                              else
         | 
| 770 | 
            +
                                ["error", error_code]
         | 
| 771 | 
            +
                              end
         | 
| 772 772 |  | 
| 773 773 | 
             
                    if respond_to?(:"oauth_#{error_code}_message")
         | 
| 774 774 | 
             
                      message = send(:"oauth_#{error_code}_message")
         | 
| 775 | 
            -
                       | 
| 775 | 
            +
                      params << ["error_description", CGI.escape(message)]
         | 
| 776 776 | 
             
                    end
         | 
| 777 777 |  | 
| 778 778 | 
             
                    state = param_or_nil("state")
         | 
| 779 779 |  | 
| 780 | 
            -
                     | 
| 780 | 
            +
                    params << ["state", state] if state
         | 
| 781 781 |  | 
| 782 | 
            -
                    _redirect_response_error(redirect_url,  | 
| 782 | 
            +
                    _redirect_response_error(redirect_url, params)
         | 
| 783 783 | 
             
                  end
         | 
| 784 784 | 
             
                end
         | 
| 785 785 |  | 
| 786 | 
            -
                def _redirect_response_error(redirect_url,  | 
| 787 | 
            -
                   | 
| 788 | 
            -
                   | 
| 789 | 
            -
                  redirect_url.query =  | 
| 786 | 
            +
                def _redirect_response_error(redirect_url, params)
         | 
| 787 | 
            +
                  params = params.map { |k, v| "#{k}=#{v}" }
         | 
| 788 | 
            +
                  params << redirect_url.query if redirect_url.query
         | 
| 789 | 
            +
                  redirect_url.query = params.join("&")
         | 
| 790 790 | 
             
                  redirect(redirect_url.to_s)
         | 
| 791 791 | 
             
                end
         | 
| 792 792 |  | 
| @@ -841,10 +841,10 @@ module Rodauth | |
| 841 841 | 
             
                  throw_json_response_error(oauth_authorization_required_error_status, "invalid_client")
         | 
| 842 842 | 
             
                end
         | 
| 843 843 |  | 
| 844 | 
            -
                def check_valid_scopes?
         | 
| 845 | 
            -
                  return false unless  | 
| 844 | 
            +
                def check_valid_scopes?(scp = scopes)
         | 
| 845 | 
            +
                  return false unless scp
         | 
| 846 846 |  | 
| 847 | 
            -
                  ( | 
| 847 | 
            +
                  (scp - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
         | 
| 848 848 | 
             
                end
         | 
| 849 849 |  | 
| 850 850 | 
             
                def check_valid_uri?(uri)
         | 
| @@ -118,8 +118,8 @@ module Rodauth | |
| 118 118 | 
             
                  }
         | 
| 119 119 | 
             
                end
         | 
| 120 120 |  | 
| 121 | 
            -
                def validate_client_registration_params
         | 
| 122 | 
            -
                  @oauth_application_params =  | 
| 121 | 
            +
                def validate_client_registration_params(request_params = request.params)
         | 
| 122 | 
            +
                  @oauth_application_params = request_params.each_with_object({}) do |(key, value), params|
         | 
| 123 123 | 
             
                    case key
         | 
| 124 124 | 
             
                    when "redirect_uris"
         | 
| 125 125 | 
             
                      if value.is_a?(Array)
         | 
| @@ -152,7 +152,7 @@ module Rodauth | |
| 152 152 | 
             
                      key = oauth_applications_grant_types_column
         | 
| 153 153 | 
             
                    when "response_types"
         | 
| 154 154 | 
             
                      if value.is_a?(Array)
         | 
| 155 | 
            -
                        grant_types =  | 
| 155 | 
            +
                        grant_types = request_params["grant_types"] || %w[authorization_code]
         | 
| 156 156 | 
             
                        value = value.each do |response_type|
         | 
| 157 157 | 
             
                          unless oauth_response_types_supported.include?(response_type)
         | 
| 158 158 | 
             
                            register_throw_json_response_error("invalid_client_metadata",
         | 
| @@ -172,7 +172,7 @@ module Rodauth | |
| 172 172 | 
             
                      when "client_uri"
         | 
| 173 173 | 
             
                        key = oauth_applications_homepage_url_column
         | 
| 174 174 | 
             
                      when "jwks_uri"
         | 
| 175 | 
            -
                        if  | 
| 175 | 
            +
                        if request_params.key?("jwks")
         | 
| 176 176 | 
             
                          register_throw_json_response_error("invalid_client_metadata",
         | 
| 177 177 | 
             
                                                             register_invalid_jwks_param_message(key, "jwks"))
         | 
| 178 178 | 
             
                        end
         | 
| @@ -180,7 +180,7 @@ module Rodauth | |
| 180 180 | 
             
                      key = __send__(:"oauth_applications_#{key}_column")
         | 
| 181 181 | 
             
                    when "jwks"
         | 
| 182 182 | 
             
                      register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(value)) unless value.is_a?(Hash)
         | 
| 183 | 
            -
                      if  | 
| 183 | 
            +
                      if request_params.key?("jwks_uri")
         | 
| 184 184 | 
             
                        register_throw_json_response_error("invalid_client_metadata",
         | 
| 185 185 | 
             
                                                           register_invalid_jwks_param_message(key, "jwks_uri"))
         | 
| 186 186 | 
             
                      end
         | 
| @@ -205,14 +205,14 @@ module Rodauth | |
| 205 205 | 
             
                        register_throw_json_response_error("invalid_client_metadata",
         | 
| 206 206 | 
             
                                                           register_invalid_param_message(key))
         | 
| 207 207 | 
             
                      end
         | 
| 208 | 
            -
                       | 
| 208 | 
            +
                      request_params[key] = value = convert_to_boolean(key, value)
         | 
| 209 209 |  | 
| 210 210 | 
             
                      key = oauth_applications_require_pushed_authorization_requests_column
         | 
| 211 211 | 
             
                    when "tls_client_certificate_bound_access_tokens"
         | 
| 212 212 | 
             
                      property = :oauth_applications_tls_client_certificate_bound_access_tokens_column
         | 
| 213 213 | 
             
                      register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key)) unless respond_to?(property)
         | 
| 214 214 |  | 
| 215 | 
            -
                       | 
| 215 | 
            +
                      request_params[key] = value = convert_to_boolean(key, value)
         | 
| 216 216 |  | 
| 217 217 | 
             
                      key = oauth_applications_tls_client_certificate_bound_access_tokens_column
         | 
| 218 218 | 
             
                    when /\Atls_client_auth_/
         | 
| @@ -20,6 +20,24 @@ module Rodauth | |
| 20 20 |  | 
| 21 21 | 
             
                private
         | 
| 22 22 |  | 
| 23 | 
            +
                def validate_authorize_params
         | 
| 24 | 
            +
                  super
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  response_mode = param_or_nil("response_mode")
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  return unless response_mode
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  response_type = param_or_nil("response_type")
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  return unless response_type == "token"
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  redirect_response_error("invalid_request") unless oauth_response_modes_for_token_supported.include?(response_mode)
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def oauth_response_modes_for_token_supported
         | 
| 38 | 
            +
                  %w[fragment]
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 23 41 | 
             
                def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
         | 
| 24 42 | 
             
                  response_type = param("response_type")
         | 
| 25 43 | 
             
                  return super unless response_type == "token" && supported_response_type?(response_type)
         | 
| @@ -48,13 +66,13 @@ module Rodauth | |
| 48 66 | 
             
                  generate_token(grant_params, false)
         | 
| 49 67 | 
             
                end
         | 
| 50 68 |  | 
| 51 | 
            -
                def _redirect_response_error(redirect_url,  | 
| 69 | 
            +
                def _redirect_response_error(redirect_url, params)
         | 
| 52 70 | 
             
                  response_types = param("response_type").split(/ +/)
         | 
| 53 71 |  | 
| 54 72 | 
             
                  return super if response_types.empty? || response_types == %w[code]
         | 
| 55 73 |  | 
| 56 | 
            -
                   | 
| 57 | 
            -
                  redirect_url.fragment =  | 
| 74 | 
            +
                  params = params.map { |k, v| "#{k}=#{v}" }
         | 
| 75 | 
            +
                  redirect_url.fragment = params.join("&")
         | 
| 58 76 | 
             
                  redirect(redirect_url.to_s)
         | 
| 59 77 | 
             
                end
         | 
| 60 78 |  | 
| @@ -62,7 +80,7 @@ module Rodauth | |
| 62 80 | 
             
                  return super unless mode == "fragment"
         | 
| 63 81 |  | 
| 64 82 | 
             
                  redirect_url = URI.parse(redirect_uri)
         | 
| 65 | 
            -
                  params = params | 
| 83 | 
            +
                  params = [URI.encode_www_form(params)]
         | 
| 66 84 | 
             
                  params << redirect_url.query if redirect_url.query
         | 
| 67 85 | 
             
                  redirect_url.fragment = params.join("&")
         | 
| 68 86 | 
             
                  redirect(redirect_url.to_s)
         | 
| @@ -0,0 +1,126 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "rodauth/oauth"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Rodauth
         | 
| 6 | 
            +
              Feature.define(:oauth_jwt_secured_authorization_response_mode, :OauthJwtSecuredAuthorizationResponseMode) do
         | 
| 7 | 
            +
                depends :oauth_authorize_base, :oauth_jwt_base
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                auth_value_method :oauth_authorization_response_mode_expires_in, 60 * 5 # 5 minutes
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                auth_value_method :oauth_applications_authorization_signed_response_alg_column, :authorization_signed_response_alg
         | 
| 12 | 
            +
                auth_value_method :oauth_applications_authorization_encrypted_response_alg_column, :authorization_encrypted_response_alg
         | 
| 13 | 
            +
                auth_value_method :oauth_applications_authorization_encrypted_response_enc_column, :authorization_encrypted_response_enc
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                auth_value_methods(
         | 
| 16 | 
            +
                  :authorization_signing_alg_values_supported,
         | 
| 17 | 
            +
                  :authorization_encryption_alg_values_supported,
         | 
| 18 | 
            +
                  :authorization_encryption_enc_values_supported
         | 
| 19 | 
            +
                )
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def oauth_response_modes_supported
         | 
| 22 | 
            +
                  jwt_response_modes = %w[jwt]
         | 
| 23 | 
            +
                  jwt_response_modes.push("query.jwt", "form_post.jwt") if features.include?(:oauth_authorization_code_grant)
         | 
| 24 | 
            +
                  jwt_response_modes << "fragment.jwt" if features.include?(:oauth_implicit_grant)
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  super | jwt_response_modes
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def authorization_signing_alg_values_supported
         | 
| 30 | 
            +
                  oauth_jwt_jws_algorithms_supported
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def authorization_encryption_alg_values_supported
         | 
| 34 | 
            +
                  oauth_jwt_jwe_algorithms_supported
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def authorization_encryption_enc_values_supported
         | 
| 38 | 
            +
                  oauth_jwt_jwe_encryption_methods_supported
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                private
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def oauth_response_modes_for_code_supported
         | 
| 44 | 
            +
                  return [] unless features.include?(:oauth_authorization_code_grant)
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  super | %w[query.jwt form_post.jwt jwt]
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def oauth_response_modes_for_token_supported
         | 
| 50 | 
            +
                  return [] unless features.include?(:oauth_implicit_grant)
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  super | %w[fragment.jwt jwt]
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def authorize_response(params, mode)
         | 
| 56 | 
            +
                  return super unless mode.end_with?("jwt")
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  response_type = param_or_nil("response_type")
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  redirect_url = URI.parse(redirect_uri)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  jwt = jwt_encode_authorization_response_mode(params)
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  if mode == "query.jwt" || (mode == "jwt" && response_type == "code")
         | 
| 65 | 
            +
                    return super unless features.include?(:oauth_authorization_code_grant)
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    params = ["response=#{CGI.escape(jwt)}"]
         | 
| 68 | 
            +
                    params << redirect_url.query if redirect_url.query
         | 
| 69 | 
            +
                    redirect_url.query = params.join("&")
         | 
| 70 | 
            +
                    redirect(redirect_url.to_s)
         | 
| 71 | 
            +
                  elsif mode == "form_post.jwt"
         | 
| 72 | 
            +
                    return super unless features.include?(:oauth_authorization_code_grant)
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                    response["Content-Type"] = "text/html"
         | 
| 75 | 
            +
                    body = form_post_response_html(redirect_url) do
         | 
| 76 | 
            +
                      "<input type=\"hidden\" name=\"response\" value=\"#{scope.h(jwt)}\" />"
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
                    response.write(body)
         | 
| 79 | 
            +
                    request.halt
         | 
| 80 | 
            +
                  elsif mode == "fragment.jwt" || (mode == "jwt" && response_type == "token")
         | 
| 81 | 
            +
                    return super unless features.include?(:oauth_implicit_grant)
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                    params = ["response=#{CGI.escape(jwt)}"]
         | 
| 84 | 
            +
                    params << redirect_url.query if redirect_url.query
         | 
| 85 | 
            +
                    redirect_url.fragment = params.join("&")
         | 
| 86 | 
            +
                    redirect(redirect_url.to_s)
         | 
| 87 | 
            +
                  else
         | 
| 88 | 
            +
                    super
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def _redirect_response_error(redirect_url, params)
         | 
| 93 | 
            +
                  response_mode = param_or_nil("response_mode")
         | 
| 94 | 
            +
                  return super unless response_mode.end_with?("jwt")
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                  authorize_response(Hash[params], response_mode)
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                def jwt_encode_authorization_response_mode(params)
         | 
| 100 | 
            +
                  now = Time.now.to_i
         | 
| 101 | 
            +
                  claims = {
         | 
| 102 | 
            +
                    iss: oauth_jwt_issuer,
         | 
| 103 | 
            +
                    aud: oauth_application[oauth_applications_client_id_column],
         | 
| 104 | 
            +
                    exp: now + oauth_authorization_response_mode_expires_in,
         | 
| 105 | 
            +
                    iat: now
         | 
| 106 | 
            +
                  }.merge(params)
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  encode_params = {
         | 
| 109 | 
            +
                    jwks: oauth_application_jwks(oauth_application),
         | 
| 110 | 
            +
                    signing_algorithm: oauth_application[oauth_applications_authorization_signed_response_alg_column],
         | 
| 111 | 
            +
                    encryption_algorithm: oauth_application[oauth_applications_authorization_encrypted_response_alg_column],
         | 
| 112 | 
            +
                    encryption_method: oauth_application[oauth_applications_authorization_encrypted_response_enc_column]
         | 
| 113 | 
            +
                  }.compact
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  jwt_encode(claims, **encode_params)
         | 
| 116 | 
            +
                end
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                def oauth_server_metadata_body(*)
         | 
| 119 | 
            +
                  super.tap do |data|
         | 
| 120 | 
            +
                    data[:authorization_signing_alg_values_supported] = authorization_signing_alg_values_supported
         | 
| 121 | 
            +
                    data[:authorization_encryption_alg_values_supported] = authorization_encryption_alg_values_supported
         | 
| 122 | 
            +
                    data[:authorization_encryption_enc_values_supported] = authorization_encryption_enc_values_supported
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
              end
         | 
| 126 | 
            +
            end
         | 
| @@ -23,9 +23,7 @@ module Rodauth | |
| 23 23 | 
             
                  classes += " disabled" if current || !page
         | 
| 24 24 | 
             
                  classes += " active" if current
         | 
| 25 25 | 
             
                  if page
         | 
| 26 | 
            -
                    params = request.GET.merge("page" => page) | 
| 27 | 
            -
                      v ? "#{CGI.escape(String(k))}=#{CGI.escape(String(v))}" : CGI.escape(String(k))
         | 
| 28 | 
            -
                    end.join("&")
         | 
| 26 | 
            +
                    params = URI.encode_www_form(request.GET.merge("page" => page))
         | 
| 29 27 |  | 
| 30 28 | 
             
                    href = "#{request.path}?#{params}"
         | 
| 31 29 |  | 
| @@ -63,7 +63,7 @@ module Rodauth | |
| 63 63 | 
             
                  id_token_signing_alg_values_supported
         | 
| 64 64 | 
             
                ].freeze
         | 
| 65 65 |  | 
| 66 | 
            -
                depends :account_expiration, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant
         | 
| 66 | 
            +
                depends :account_expiration, :oauth_jwt, :oauth_jwt_jwks, :oauth_authorization_code_grant, :oauth_implicit_grant
         | 
| 67 67 |  | 
| 68 68 | 
             
                auth_value_method :oauth_application_scopes, %w[openid]
         | 
| 69 69 |  | 
| @@ -89,9 +89,16 @@ module Rodauth | |
| 89 89 | 
             
                auth_value_method :oauth_prompt_login_interval, 5 * 60 * 60 # 5 minutes
         | 
| 90 90 |  | 
| 91 91 | 
             
                auth_value_methods(
         | 
| 92 | 
            +
                  :userinfo_signing_alg_values_supported,
         | 
| 93 | 
            +
                  :userinfo_encryption_alg_values_supported,
         | 
| 94 | 
            +
                  :userinfo_encryption_enc_values_supported,
         | 
| 95 | 
            +
                  :request_object_signing_alg_values_supported,
         | 
| 96 | 
            +
                  :request_object_encryption_alg_values_supported,
         | 
| 97 | 
            +
                  :request_object_encryption_enc_values_supported,
         | 
| 92 98 | 
             
                  :oauth_acr_values_supported,
         | 
| 93 99 | 
             
                  :get_oidc_account_last_login_at,
         | 
| 94 100 | 
             
                  :oidc_authorize_on_prompt_none?,
         | 
| 101 | 
            +
                  :fill_with_account_claims,
         | 
| 95 102 | 
             
                  :get_oidc_param,
         | 
| 96 103 | 
             
                  :get_additional_param,
         | 
| 97 104 | 
             
                  :require_acr_value_phr,
         | 
| @@ -233,6 +240,30 @@ module Rodauth | |
| 233 240 | 
             
                  end
         | 
| 234 241 | 
             
                end
         | 
| 235 242 |  | 
| 243 | 
            +
                def userinfo_signing_alg_values_supported
         | 
| 244 | 
            +
                  oauth_jwt_jws_algorithms_supported
         | 
| 245 | 
            +
                end
         | 
| 246 | 
            +
             | 
| 247 | 
            +
                def userinfo_encryption_alg_values_supported
         | 
| 248 | 
            +
                  oauth_jwt_jwe_algorithms_supported
         | 
| 249 | 
            +
                end
         | 
| 250 | 
            +
             | 
| 251 | 
            +
                def userinfo_encryption_enc_values_supported
         | 
| 252 | 
            +
                  oauth_jwt_jwe_encryption_methods_supported
         | 
| 253 | 
            +
                end
         | 
| 254 | 
            +
             | 
| 255 | 
            +
                def request_object_signing_alg_values_supported
         | 
| 256 | 
            +
                  oauth_jwt_jws_algorithms_supported
         | 
| 257 | 
            +
                end
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                def request_object_encryption_alg_values_supported
         | 
| 260 | 
            +
                  oauth_jwt_jwe_algorithms_supported
         | 
| 261 | 
            +
                end
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                def request_object_encryption_enc_values_supported
         | 
| 264 | 
            +
                  oauth_jwt_jwe_encryption_methods_supported
         | 
| 265 | 
            +
                end
         | 
| 266 | 
            +
             | 
| 236 267 | 
             
                def oauth_acr_values_supported
         | 
| 237 268 | 
             
                  acr_values = []
         | 
| 238 269 | 
             
                  acr_values << "phrh" if features.include?(:webauthn_login)
         | 
| @@ -274,29 +305,33 @@ module Rodauth | |
| 274 305 |  | 
| 275 306 | 
             
                  sc = scopes
         | 
| 276 307 |  | 
| 277 | 
            -
                   | 
| 278 | 
            -
             | 
| 308 | 
            +
                  # MUST ensure that the prompt parameter contains consent
         | 
| 309 | 
            +
                  # MUST ignore the offline_access request unless the Client
         | 
| 310 | 
            +
                  # is using a response_type value that would result in an
         | 
| 311 | 
            +
                  # Authorization Code
         | 
| 312 | 
            +
                  if sc && sc.include?("offline_access") && !(param_or_nil("prompt") == "consent" && (
         | 
| 313 | 
            +
                            (response_type = param_or_nil("response_type")) && response_type.split(" ").include?("code")
         | 
| 314 | 
            +
                          ))
         | 
| 279 315 | 
             
                    sc.delete("offline_access")
         | 
| 280 316 |  | 
| 281 | 
            -
                    # MUST ensure that the prompt parameter contains consent
         | 
| 282 | 
            -
                    # MUST ignore the offline_access request unless the Client
         | 
| 283 | 
            -
                    # is using a response_type value that would result in an
         | 
| 284 | 
            -
                    # Authorization Code
         | 
| 285 | 
            -
                    if param_or_nil("prompt") == "consent" && (
         | 
| 286 | 
            -
                      (response_type = param_or_nil("response_type")) && response_type.split(" ").include?("code")
         | 
| 287 | 
            -
                    )
         | 
| 288 | 
            -
                      request.params["access_type"] = "offline"
         | 
| 289 | 
            -
                    end
         | 
| 290 | 
            -
             | 
| 291 317 | 
             
                    request.params["scope"] = sc.join(" ")
         | 
| 292 318 | 
             
                  end
         | 
| 293 319 |  | 
| 294 320 | 
             
                  super
         | 
| 295 321 |  | 
| 296 | 
            -
                   | 
| 297 | 
            -
             | 
| 322 | 
            +
                  response_type = param_or_nil("response_type")
         | 
| 323 | 
            +
             | 
| 324 | 
            +
                  is_id_token_response_type = response_type.include?("id_token")
         | 
| 325 | 
            +
             | 
| 326 | 
            +
                  redirect_response_error("invalid_request") if is_id_token_response_type && !param_or_nil("nonce")
         | 
| 327 | 
            +
             | 
| 328 | 
            +
                  return unless is_id_token_response_type || response_type == "code token"
         | 
| 329 | 
            +
             | 
| 330 | 
            +
                  response_mode = param_or_nil("response_mode")
         | 
| 331 | 
            +
             | 
| 332 | 
            +
                  # id_token: The default Response Mode for this Response Type is the fragment encoding and the query encoding MUST NOT be used.
         | 
| 298 333 |  | 
| 299 | 
            -
                  redirect_response_error("invalid_request") unless  | 
| 334 | 
            +
                  redirect_response_error("invalid_request") unless response_mode.nil? || response_mode == "fragment"
         | 
| 300 335 | 
             
                end
         | 
| 301 336 |  | 
| 302 337 | 
             
                def require_authorizable_account
         | 
| @@ -463,24 +498,7 @@ module Rodauth | |
| 463 498 | 
             
                  signing_algorithm = oauth_application[oauth_applications_id_token_signed_response_alg_column] ||
         | 
| 464 499 | 
             
                                      oauth_jwt_keys.keys.first
         | 
| 465 500 |  | 
| 466 | 
            -
                   | 
| 467 | 
            -
             | 
| 468 | 
            -
                  id_token_claims[:nonce] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
         | 
| 469 | 
            -
             | 
| 470 | 
            -
                  id_token_claims[:acr] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
         | 
| 471 | 
            -
             | 
| 472 | 
            -
                  # Time when the End-User authentication occurred.
         | 
| 473 | 
            -
                  id_token_claims[:auth_time] = get_oidc_account_last_login_at(oauth_grant[oauth_grants_account_id_column]).to_i
         | 
| 474 | 
            -
             | 
| 475 | 
            -
                  # Access Token hash value.
         | 
| 476 | 
            -
                  if (access_token = oauth_grant[oauth_grants_token_column])
         | 
| 477 | 
            -
                    id_token_claims[:at_hash] = id_token_hash(access_token, signing_algorithm)
         | 
| 478 | 
            -
                  end
         | 
| 479 | 
            -
             | 
| 480 | 
            -
                  # code hash value.
         | 
| 481 | 
            -
                  if (code = oauth_grant[oauth_grants_code_column])
         | 
| 482 | 
            -
                    id_token_claims[:c_hash] = id_token_hash(code, signing_algorithm)
         | 
| 483 | 
            -
                  end
         | 
| 501 | 
            +
                  id_claims = id_token_claims(oauth_grant, signing_algorithm)
         | 
| 484 502 |  | 
| 485 503 | 
             
                  account = db[accounts_table].where(account_id_column => oauth_grant[oauth_grants_account_id_column]).first
         | 
| 486 504 |  | 
| @@ -500,7 +518,7 @@ module Rodauth | |
| 500 518 |  | 
| 501 519 | 
             
                  # 5.4 - However, when no Access Token is issued (which is the case for the response_type value id_token),
         | 
| 502 520 | 
             
                  # the resulting Claims are returned in the ID Token.
         | 
| 503 | 
            -
                  fill_with_account_claims( | 
| 521 | 
            +
                  fill_with_account_claims(id_claims, account, oauth_scopes, param_or_nil("claims_locales")) if include_claims
         | 
| 504 522 |  | 
| 505 523 | 
             
                  params = {
         | 
| 506 524 | 
             
                    jwks: oauth_application_jwks(oauth_application),
         | 
| @@ -509,7 +527,30 @@ module Rodauth | |
| 509 527 | 
             
                    encryption_method: oauth_application[oauth_applications_id_token_encrypted_response_enc_column]
         | 
| 510 528 | 
             
                  }.compact
         | 
| 511 529 |  | 
| 512 | 
            -
                  oauth_grant[:id_token] = jwt_encode( | 
| 530 | 
            +
                  oauth_grant[:id_token] = jwt_encode(id_claims, **params)
         | 
| 531 | 
            +
                end
         | 
| 532 | 
            +
             | 
| 533 | 
            +
                def id_token_claims(oauth_grant, signing_algorithm)
         | 
| 534 | 
            +
                  claims = jwt_claims(oauth_grant)
         | 
| 535 | 
            +
             | 
| 536 | 
            +
                  claims[:nonce] = oauth_grant[oauth_grants_nonce_column] if oauth_grant[oauth_grants_nonce_column]
         | 
| 537 | 
            +
             | 
| 538 | 
            +
                  claims[:acr] = oauth_grant[oauth_grants_acr_column] if oauth_grant[oauth_grants_acr_column]
         | 
| 539 | 
            +
             | 
| 540 | 
            +
                  # Time when the End-User authentication occurred.
         | 
| 541 | 
            +
                  claims[:auth_time] = get_oidc_account_last_login_at(oauth_grant[oauth_grants_account_id_column]).to_i
         | 
| 542 | 
            +
             | 
| 543 | 
            +
                  # Access Token hash value.
         | 
| 544 | 
            +
                  if (access_token = oauth_grant[oauth_grants_token_column])
         | 
| 545 | 
            +
                    claims[:at_hash] = id_token_hash(access_token, signing_algorithm)
         | 
| 546 | 
            +
                  end
         | 
| 547 | 
            +
             | 
| 548 | 
            +
                  # code hash value.
         | 
| 549 | 
            +
                  if (code = oauth_grant[oauth_grants_code_column])
         | 
| 550 | 
            +
                    claims[:c_hash] = id_token_hash(code, signing_algorithm)
         | 
| 551 | 
            +
                  end
         | 
| 552 | 
            +
             | 
| 553 | 
            +
                  claims
         | 
| 513 554 | 
             
                end
         | 
| 514 555 |  | 
| 515 556 | 
             
                # aka fill_with_standard_claims
         | 
| @@ -627,10 +668,9 @@ module Rodauth | |
| 627 668 |  | 
| 628 669 | 
             
                def check_valid_response_type?
         | 
| 629 670 | 
             
                  case param_or_nil("response_type")
         | 
| 630 | 
            -
                  when "none", "id_token", "code id_token" # multiple
         | 
| 671 | 
            +
                  when "none", "id_token", "code id_token", # multiple
         | 
| 672 | 
            +
                       "code token", "id_token token", "code id_token token"
         | 
| 631 673 | 
             
                    true
         | 
| 632 | 
            -
                  when "code token", "id_token token", "code id_token token"
         | 
| 633 | 
            -
                    supports_token_response_type?
         | 
| 634 674 | 
             
                  else
         | 
| 635 675 | 
             
                    super
         | 
| 636 676 | 
             
                  end
         | 
| @@ -642,10 +682,6 @@ module Rodauth | |
| 642 682 | 
             
                  param("response_type") == "none"
         | 
| 643 683 | 
             
                end
         | 
| 644 684 |  | 
| 645 | 
            -
                def supports_token_response_type?
         | 
| 646 | 
            -
                  features.include?(:oauth_implicit_grant)
         | 
| 647 | 
            -
                end
         | 
| 648 | 
            -
             | 
| 649 685 | 
             
                def do_authorize(response_params = {}, response_mode = param_or_nil("response_mode"))
         | 
| 650 686 | 
             
                  response_type = param("response_type")
         | 
| 651 687 | 
             
                  case response_type
         | 
| @@ -654,8 +690,6 @@ module Rodauth | |
| 654 690 | 
             
                    generate_id_token(grant_params, true)
         | 
| 655 691 | 
             
                    response_params.replace("id_token" => grant_params[:id_token])
         | 
| 656 692 | 
             
                  when "code token"
         | 
| 657 | 
            -
                    redirect_response_error("invalid_request") unless supports_token_response_type?
         | 
| 658 | 
            -
             | 
| 659 693 | 
             
                    response_params.replace(create_oauth_grant_with_token)
         | 
| 660 694 | 
             
                  when "code id_token"
         | 
| 661 695 | 
             
                    params = _do_authorize_code
         | 
| @@ -666,16 +700,12 @@ module Rodauth | |
| 666 700 | 
             
                      "code" => params["code"]
         | 
| 667 701 | 
             
                    )
         | 
| 668 702 | 
             
                  when "id_token token"
         | 
| 669 | 
            -
                    redirect_response_error("invalid_request") unless supports_token_response_type?
         | 
| 670 | 
            -
             | 
| 671 703 | 
             
                    grant_params = oidc_grant_params.merge(oauth_grants_type_column => "hybrid")
         | 
| 672 704 | 
             
                    oauth_grant = _do_authorize_token(grant_params)
         | 
| 673 705 | 
             
                    generate_id_token(oauth_grant)
         | 
| 674 706 |  | 
| 675 707 | 
             
                    response_params.replace(json_access_token_payload(oauth_grant))
         | 
| 676 708 | 
             
                  when "code id_token token"
         | 
| 677 | 
            -
                    redirect_response_error("invalid_request") unless supports_token_response_type?
         | 
| 678 | 
            -
             | 
| 679 709 | 
             
                    params = create_oauth_grant_with_token
         | 
| 680 710 | 
             
                    oauth_grant = valid_oauth_grant_ds.where(oauth_grants_code_column => params["code"]).first
         | 
| 681 711 | 
             
                    oauth_grant[oauth_grants_token_column] = params["access_token"]
         | 
| @@ -694,7 +724,8 @@ module Rodauth | |
| 694 724 | 
             
                  grant_params = {
         | 
| 695 725 | 
             
                    **resource_owner_params,
         | 
| 696 726 | 
             
                    oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
         | 
| 697 | 
            -
                    oauth_grants_scopes_column => scopes.join(oauth_scope_separator)
         | 
| 727 | 
            +
                    oauth_grants_scopes_column => scopes.join(oauth_scope_separator),
         | 
| 728 | 
            +
                    oauth_grants_redirect_uri_column => param_or_nil("redirect_uri")
         | 
| 698 729 | 
             
                  }
         | 
| 699 730 | 
             
                  if (nonce = param_or_nil("nonce"))
         | 
| 700 731 | 
             
                    grant_params[oauth_grants_nonce_column] = nonce
         | 
| @@ -709,6 +740,12 @@ module Rodauth | |
| 709 740 | 
             
                  grant_params
         | 
| 710 741 | 
             
                end
         | 
| 711 742 |  | 
| 743 | 
            +
                def generate_token(grant_params = {}, should_generate_refresh_token = true)
         | 
| 744 | 
            +
                  scopes = grant_params[oauth_grants_scopes_column].split(oauth_scope_separator)
         | 
| 745 | 
            +
             | 
| 746 | 
            +
                  super(grant_params, scopes.include?("offline_access") && should_generate_refresh_token)
         | 
| 747 | 
            +
                end
         | 
| 748 | 
            +
             | 
| 712 749 | 
             
                def authorize_response(params, mode)
         | 
| 713 750 | 
             
                  redirect_url = URI.parse(redirect_uri)
         | 
| 714 751 | 
             
                  redirect(redirect_url.to_s) if mode == "none"
         | 
| @@ -10,7 +10,7 @@ module Rodauth | |
| 10 10 |  | 
| 11 11 | 
             
                private
         | 
| 12 12 |  | 
| 13 | 
            -
                def validate_client_registration_params
         | 
| 13 | 
            +
                def validate_client_registration_params(*)
         | 
| 14 14 | 
             
                  super
         | 
| 15 15 |  | 
| 16 16 | 
             
                  if (value = @oauth_application_params[oauth_applications_application_type_column])
         | 
| @@ -174,6 +174,44 @@ module Rodauth | |
| 174 174 | 
             
                    register_throw_json_response_error("invalid_client_metadata",
         | 
| 175 175 | 
             
                                                       register_invalid_client_metadata_message("userinfo_encrypted_response_enc", value))
         | 
| 176 176 | 
             
                  end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                  if features.include?(:oauth_jwt_secured_authorization_response_mode)
         | 
| 179 | 
            +
                    if defined?(oauth_applications_authorization_signed_response_alg_column) &&
         | 
| 180 | 
            +
                       (value = @oauth_application_params[oauth_applications_authorization_signed_response_alg_column]) &&
         | 
| 181 | 
            +
                       (!oauth_jwt_jws_algorithms_supported.include?(value) || value == "none")
         | 
| 182 | 
            +
                      register_throw_json_response_error("invalid_client_metadata",
         | 
| 183 | 
            +
                                                         register_invalid_client_metadata_message("authorization_signed_response_alg", value))
         | 
| 184 | 
            +
                    end
         | 
| 185 | 
            +
             | 
| 186 | 
            +
                    if defined?(oauth_applications_authorization_encrypted_response_alg_column) &&
         | 
| 187 | 
            +
                       (value = @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]) &&
         | 
| 188 | 
            +
                       !oauth_jwt_jwe_algorithms_supported.include?(value)
         | 
| 189 | 
            +
                      register_throw_json_response_error("invalid_client_metadata",
         | 
| 190 | 
            +
                                                         register_invalid_client_metadata_message("authorization_encrypted_response_alg", value))
         | 
| 191 | 
            +
                    end
         | 
| 192 | 
            +
             | 
| 193 | 
            +
                    if defined?(oauth_applications_authorization_encrypted_response_enc_column)
         | 
| 194 | 
            +
                      if (value = @oauth_application_params[oauth_applications_authorization_encrypted_response_enc_column])
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                        unless @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]
         | 
| 197 | 
            +
                          # When authorization_encrypted_response_enc is included, authorization_encrypted_response_alg MUST also be provided.
         | 
| 198 | 
            +
                          register_throw_json_response_error("invalid_client_metadata",
         | 
| 199 | 
            +
                                                             register_invalid_client_metadata_message("authorization_encrypted_response_alg", value))
         | 
| 200 | 
            +
             | 
| 201 | 
            +
                        end
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                        unless oauth_jwt_jwe_encryption_methods_supported.include?(value)
         | 
| 204 | 
            +
                          register_throw_json_response_error("invalid_client_metadata",
         | 
| 205 | 
            +
                                                             register_invalid_client_metadata_message("authorization_encrypted_response_enc", value))
         | 
| 206 | 
            +
                        end
         | 
| 207 | 
            +
                      elsif @oauth_application_params[oauth_applications_authorization_encrypted_response_alg_column]
         | 
| 208 | 
            +
                        # If authorization_encrypted_response_alg is specified, the default for this value is A128CBC-HS256.
         | 
| 209 | 
            +
                        @oauth_application_params[oauth_applications_authorization_encrypted_response_enc_column] = "A128CBC-HS256"
         | 
| 210 | 
            +
                      end
         | 
| 211 | 
            +
                    end
         | 
| 212 | 
            +
                  end
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                  @oauth_application_params
         | 
| 177 215 | 
             
                end
         | 
| 178 216 |  | 
| 179 217 | 
             
                def validate_client_registration_response_type(response_type, grant_types)
         | 
| @@ -0,0 +1,73 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "rodauth/oauth"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Rodauth
         | 
| 6 | 
            +
              Feature.define(:oidc_self_issued, :OidcSelfIssued) do
         | 
| 7 | 
            +
                depends :oidc, :oidc_dynamic_client_registration
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                auth_value_method :oauth_application_scopes, %w[openid profile email address phone]
         | 
| 10 | 
            +
                auth_value_method :oauth_jwt_jws_algorithms_supported, %w[RS256]
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                SELF_ISSUED_DEFAULT_APPLICATION_PARAMS = {
         | 
| 13 | 
            +
                  "scope" => "openid profile email address phone",
         | 
| 14 | 
            +
                  "response_types" => ["id_token"],
         | 
| 15 | 
            +
                  "subject_type" => "pairwise",
         | 
| 16 | 
            +
                  "id_token_signed_response_alg" => "RS256",
         | 
| 17 | 
            +
                  "request_object_signing_alg" => "RS256",
         | 
| 18 | 
            +
                  "grant_types" => %w[implicit]
         | 
| 19 | 
            +
                }.freeze
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def oauth_application
         | 
| 22 | 
            +
                  return @oauth_application if defined?(@oauth_application)
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  return super unless (registration = param_or_nil("registration"))
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  # self-issued!
         | 
| 27 | 
            +
                  redirect_uri = param_or_nil("client_id")
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  registration_params = JSON.parse(registration)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  registration_params = SELF_ISSUED_DEFAULT_APPLICATION_PARAMS.merge(registration_params)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  client_params = validate_client_registration_params(registration_params)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  request.params["redirect_uri"] = client_params[oauth_applications_client_id_column] = redirect_uri
         | 
| 36 | 
            +
                  client_params[oauth_applications_redirect_uri_column] ||= redirect_uri
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  @oauth_application = client_params
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                private
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def oauth_response_types_supported
         | 
| 44 | 
            +
                  %w[id_token]
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def request_object_signing_alg_values_supported
         | 
| 48 | 
            +
                  %w[none RS256]
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def id_token_claims(oauth_grant, signing_algorithm)
         | 
| 52 | 
            +
                  claims = super
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  return claims unless claims[:client_id] == oauth_grant[oauth_grants_redirect_uri_column]
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  # https://openid.net/specs/openid-connect-core-1_0.html#SelfIssued - 7.4
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  pub_key = oauth_jwt_public_keys[signing_algorithm]
         | 
| 59 | 
            +
                  pub_key = pub_key.first if pub_key.is_a?(Array)
         | 
| 60 | 
            +
                  claims[:sub_jwk] = sub_jwk = jwk_export(pub_key)
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  claims[:iss] = "https://self-issued.me"
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                  claims[:aud] = oauth_grant[oauth_grants_redirect_uri_column]
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  jwk_thumbprint = jwk_thumbprint(sub_jwk)
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  claims[:sub] = Base64.urlsafe_encode64(jwk_thumbprint, padding: false)
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  claims
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
              end
         | 
| 73 | 
            +
            end
         | 
    
        data/templates/authorize.str
    CHANGED
    
    | @@ -88,6 +88,7 @@ | |
| 88 88 | 
             
                #{"<input type=\"hidden\" name=\"claims_locales\" value=\"#{rodauth.param("claims_locales")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("claims_locales")}
         | 
| 89 89 | 
             
                #{"<input type=\"hidden\" name=\"claims\" value=\"#{h(rodauth.param("claims"))}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("claims")}
         | 
| 90 90 | 
             
                #{"<input type=\"hidden\" name=\"acr_values\" value=\"#{rodauth.param("acr_values")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("acr_values")}
         | 
| 91 | 
            +
                #{"<input type=\"hidden\" name=\"registration\" value=\"#{h(rodauth.param("registration"))}\"/>" if rodauth.features.include?(:oidc_self_issued) && rodauth.param_or_nil("registration")}
         | 
| 91 92 | 
             
                #{
         | 
| 92 93 | 
             
                  if rodauth.features.include?(:oauth_resource_indicators) && rodauth.resource_indicators
         | 
| 93 94 | 
             
                    rodauth.resource_indicators.map do |resource|
         | 
    
        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: 1. | 
| 4 | 
            +
              version: 1.3.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: 2023- | 
| 11 | 
            +
            date: 2023-04-01 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: rodauth
         | 
| @@ -69,6 +69,7 @@ extra_rdoc_files: | |
| 69 69 | 
             
            - doc/release_notes/1_0_0.md
         | 
| 70 70 | 
             
            - doc/release_notes/1_1_0.md
         | 
| 71 71 | 
             
            - doc/release_notes/1_2_0.md
         | 
| 72 | 
            +
            - doc/release_notes/1_3_0.md
         | 
| 72 73 | 
             
            files:
         | 
| 73 74 | 
             
            - CHANGELOG.md
         | 
| 74 75 | 
             
            - LICENSE.txt
         | 
| @@ -109,6 +110,7 @@ files: | |
| 109 110 | 
             
            - doc/release_notes/1_0_0.md
         | 
| 110 111 | 
             
            - doc/release_notes/1_1_0.md
         | 
| 111 112 | 
             
            - doc/release_notes/1_2_0.md
         | 
| 113 | 
            +
            - doc/release_notes/1_3_0.md
         | 
| 112 114 | 
             
            - lib/generators/rodauth/oauth/install_generator.rb
         | 
| 113 115 | 
             
            - lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
         | 
| 114 116 | 
             
            - lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
         | 
| @@ -138,6 +140,7 @@ files: | |
| 138 140 | 
             
            - lib/rodauth/features/oauth_jwt_bearer_grant.rb
         | 
| 139 141 | 
             
            - lib/rodauth/features/oauth_jwt_jwks.rb
         | 
| 140 142 | 
             
            - lib/rodauth/features/oauth_jwt_secured_authorization_request.rb
         | 
| 143 | 
            +
            - lib/rodauth/features/oauth_jwt_secured_authorization_response_mode.rb
         | 
| 141 144 | 
             
            - lib/rodauth/features/oauth_management_base.rb
         | 
| 142 145 | 
             
            - lib/rodauth/features/oauth_pkce.rb
         | 
| 143 146 | 
             
            - lib/rodauth/features/oauth_pushed_authorization_request.rb
         | 
| @@ -150,6 +153,7 @@ files: | |
| 150 153 | 
             
            - lib/rodauth/features/oidc.rb
         | 
| 151 154 | 
             
            - lib/rodauth/features/oidc_dynamic_client_registration.rb
         | 
| 152 155 | 
             
            - lib/rodauth/features/oidc_rp_initiated_logout.rb
         | 
| 156 | 
            +
            - lib/rodauth/features/oidc_self_issued.rb
         | 
| 153 157 | 
             
            - lib/rodauth/oauth.rb
         | 
| 154 158 | 
             
            - lib/rodauth/oauth/database_extensions.rb
         | 
| 155 159 | 
             
            - lib/rodauth/oauth/http_extensions.rb
         |