rodauth-oauth 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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