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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa95c4db43b0068e5a7c1f8af1d904a26351a007f203be233803bae0ed471f32
4
- data.tar.gz: fe0da3df41a98b6411fd20378eb41f22e5c4766cf42fd3061a45d0c6b8606bcf
3
+ metadata.gz: 4d7d5f8b68686703954bf4e335cef0ea33f9e31c94c439df84f08e8ff3270829
4
+ data.tar.gz: 1da57ba2082818a74dbca4d1c6bcab0c15f97da891e12c03a8bf91440a4edcfd
5
5
  SHA512:
6
- metadata.gz: 910bb63ff3a0be7cb035d54ed4eb48916b2137d886523403154d0eefd5d72bdb4375a268e8c94eb8047592bddfb2dd79b7022611243e18902ec33f467450e013
7
- data.tar.gz: 332125982a596d3ea0c773c7e84e5a4e4cdeed2bc9de0069f742d9b2a10de0b2182fcaea69c658fefe88b1a67e4c8b81c2dbafa3dc4c155d0100a64b1d444bb9
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);
@@ -1,4 +1,4 @@
1
- ## 1.0.0 (10/01/2023)
1
+ ## 1.1.0 (10/01/2023)
2
2
 
3
3
  ## Features
4
4
 
@@ -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.integer :account_id
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.integer :account_id
64
+ t.bigint :account_id
65
65
  t.foreign_key :accounts, column: :account_id
66
- t.integer :oauth_application_id
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.integer :oauth_application_id
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
- redirect_response_error("invalid_request") if response_mode && !oauth_response_modes_supported.include?(response_mode)
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 = params.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v)}" }
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
- scope.view layout: false, inline: <<-FORM
76
- <html>
77
- <head><title>Authorized</title></head>
78
- <body onload="javascript:document.forms[0].submit()">
79
- <form method="post" action="#{redirect_uri}">
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, query_params)
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
- response.write <<-FORM
100
- <html>
101
- <head><title></title></head>
102
- <body onload="javascript:document.forms[0].submit()">
103
- <form method="post" action="#{redirect_uri}">
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
- query_params = []
765
+ params = []
766
766
 
767
- query_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
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
- query_params << ["error_description", CGI.escape(message)]
775
+ params << ["error_description", CGI.escape(message)]
776
776
  end
777
777
 
778
778
  state = param_or_nil("state")
779
779
 
780
- query_params << ["state", state] if state
780
+ params << ["state", state] if state
781
781
 
782
- _redirect_response_error(redirect_url, query_params)
782
+ _redirect_response_error(redirect_url, params)
783
783
  end
784
784
  end
785
785
 
786
- def _redirect_response_error(redirect_url, query_params)
787
- query_params = query_params.map { |k, v| "#{k}=#{v}" }
788
- query_params << redirect_url.query if redirect_url.query
789
- redirect_url.query = query_params.join("&")
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 scopes
844
+ def check_valid_scopes?(scp = scopes)
845
+ return false unless scp
846
846
 
847
- (scopes - oauth_application[oauth_applications_scopes_column].split(oauth_scope_separator)).empty?
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 = request.params.each_with_object({}) do |(key, value), 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 = request.params["grant_types"] || %w[authorization_code]
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 request.params.key?("jwks")
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 request.params.key?("jwks_uri")
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
- request.params[key] = value = convert_to_boolean(key, value)
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
- request.params[key] = value = convert_to_boolean(key, value)
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, query_params)
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
- query_params = query_params.map { |k, v| "#{k}=#{v}" }
57
- redirect_url.fragment = query_params.join("&")
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.map { |k, v| "#{k}=#{v}" }
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).map do |k, v|
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
- if sc && sc.include?("offline_access")
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
- return unless (response_type = param_or_nil("response_type"))
297
- return unless response_type.include?("id_token")
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 param_or_nil("nonce")
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
- id_token_claims = jwt_claims(oauth_grant)
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(id_token_claims, account, oauth_scopes, param_or_nil("claims_locales")) if include_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(id_token_claims, **params)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "1.2.0"
5
+ VERSION = "1.3.0"
6
6
  end
7
7
  end
@@ -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.2.0
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-02-13 00:00:00.000000000 Z
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