rodauth-oauth 1.0.0.pre.beta1 → 1.0.0.pre.beta2

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: 299307b4879c519f6bcf7e5cfb875b75ac1d6b73b90371d3c9925baaf50dee08
4
- data.tar.gz: 05a87d3e473b514f7cbb67d841a4a9185154dd1d15933d3321a3e4946c9c28ed
3
+ metadata.gz: 0f96bced835f21567ea5603751e8cf06b53d4eae70d9cab8bc685e2fca8c2027
4
+ data.tar.gz: 59badde6a055fa2638bcb41b01534f0b5b8de9eac541ac596f25e6d5cd6fb043
5
5
  SHA512:
6
- metadata.gz: 2600d4236957c98ece6db0b06d280675d0e64c203a42e59e52063fea851bb42ec782063da5e8598211ae2a20faf9282616ffba14de8333f4e3a49ba04c97d154
7
- data.tar.gz: 7a3b5a0b1f9979c92329848d57c6de9a68f372f2357d4ade96538aea884124235efeeb3bd28d2336d3dad92344ce59cb61958c868ffa8b12f4771d17b2e1f0f7
6
+ metadata.gz: 492a5c3c12bcc678c5eb6171e9d9851db745412fdb3675674c61b512dbfd1ff1aec09da02a3d4df3acb9133148990538200c49ce05de7a34dea85107aebf151b
7
+ data.tar.gz: 65f1223065b2a0bce609b4137b8fadd206fffa9904caac97c41dc4d429528dea474a8b450128409ed164f6407c690e95a3e7c4a83b8f9302fb41d0336e132dea
data/README.md CHANGED
@@ -35,11 +35,12 @@ This gem implements the following RFCs and features of OAuth:
35
35
 
36
36
  It also implements the [OpenID Connect layer](https://openid.net/connect/) (via the `openid` feature) on top of the OAuth features it provides, including:
37
37
 
38
- * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html);
39
- * [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0-29.html);
40
- * [OpenID Multiple Response Types](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html);
41
- * [OpenID Connect Dynamic Client Registration](https://openid.net/specs/openid-connect-registration-1_0.html);
42
- * [RP Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html);
38
+ * `oidc`
39
+ * [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html);
40
+ * [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0-29.html);
41
+ * [OpenID Multiple Response Types](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html);
42
+ * `oidc_dynamic_client_registration` - [OpenID Connect Dynamic Client Registration](https://openid.net/specs/openid-connect-registration-1_0.html);
43
+ * `oidc_rp_initiated_logout` - [RP Initiated Logout](https://openid.net/specs/openid-connect-rpinitiated-1_0.html);
43
44
 
44
45
  This gem supports also rails (through [rodauth-rails]((https://github.com/janko/rodauth-rails))).
45
46
 
@@ -0,0 +1,34 @@
1
+ This version passes the conformance tests for the following OpenID Connect certification profiles:
2
+
3
+ * Basic certification
4
+ * Form-post basic certification
5
+ * Config certification
6
+ * Dynamic Config certification (`response_type=code`)
7
+
8
+ ## Breaking Changes
9
+
10
+ * homepage url is no longer a client application required property.
11
+ * OIDC RP-initiated logout extracted into `oidc_rp_initiated_logout` feature.
12
+
13
+ ## Features
14
+
15
+ * `oauth_jwt_secured_authorization_request` now supports a `request_uri` query param as well.
16
+ * `oidc` supports essential claims, via the `claims` authorization request query parameter.
17
+
18
+ ## Improvements
19
+
20
+ * exposing `acr_values_supported` in the openid configuration.
21
+ * `oauth_request_object_signing_alg_allow_none` enables `"none"` as an accepted request object signing alg when `true` (`false` by default).
22
+ * OIDC `offline_access` supported.
23
+
24
+ ## Bugfixes
25
+
26
+ * JWT: "sub" is now always a string.
27
+ * `response_type` is now an authorization request required parameter (as per the RFC).
28
+ * `state` is now passed along when redirecting from authorization requeests with `error`;
29
+ * access token can now be read from POST body or GET quety params (as per the RFC).
30
+ * id token no longer shipping with claims with `null` value;
31
+ * id token no longer encoding claims by default (only when `response_type=id_token`, as per the RFC).
32
+ * support "JWT without kid" when doing jwt decoding for JWT tokens not generated in the provider (such as request objects).
33
+ * Set `iss` and `aud` claims in the Userinfo JWT response.
34
+ * Make sure errors are also delivered via form POST, when `response_mode=form_post`.
@@ -2,7 +2,9 @@
2
2
  <% if rodauth.oauth_application[rodauth.oauth_applications_logo_uri_column] %>
3
3
  <%= image_tag rodauth.oauth_application[rodauth.oauth_applications_logo_uri_column] %>
4
4
  <% end %>
5
- <p class="lead"><%= rodauth.authorize_page_lead(name: link_to(rodauth.oauth_application[rodauth.oauth_applications_name_column], rodauth.oauth_application[rodauth.oauth_applications_homepage_url_column])).html_safe %></p>
5
+ <% application_uri = rodauth.oauth_application[rodauth.oauth_applications_homepage_url_column] %>
6
+ <% application_name = application_uri ? link_to(rodauth.oauth_application[rodauth.oauth_applications_name_column], application_uri) : rodauth.oauth_application[rodauth.oauth_applications_name_column] %>
7
+ <p class="lead"><%= rodauth.authorize_page_lead(name: application_name).html_safe %></p>
6
8
 
7
9
  <div class="list-group">
8
10
  <% if rodauth.oauth_application[rodauth.oauth_applications_tos_uri_column] %>
@@ -26,10 +28,14 @@
26
28
  <h1 class="display-6"><%= rodauth.oauth_grants_scopes_label %></h1>
27
29
 
28
30
  <% rodauth.authorize_scopes.each do |scope| %>
29
- <div class="form-check">
30
- <%= check_box_tag "scope[]", scope, id: scope, class: "form-check-input" %>
31
- <%= label_tag scope, scope, class: "form-check-label" %>
32
- </div>
31
+ <% if rodauth.features.include?(:oidc) && scope == "offline_access" %>
32
+ <%= hidden_field_tag "scope[]", scope %>
33
+ <% else %>
34
+ <div class="form-check">
35
+ <%= check_box_tag "scope[]", scope, id: scope, class: "form-check-input" %>
36
+ <%= label_tag scope, scope, class: "form-check-label" %>
37
+ </div>
38
+ <% end %>
33
39
  <% end %>
34
40
  <%= hidden_field_tag :client_id, params[:client_id] %>
35
41
  <% %i[access_type response_type response_mode state redirect_uri].each do |oauth_param| %>
@@ -51,6 +57,9 @@
51
57
  <% end %>
52
58
  <% end %>
53
59
  <% if rodauth.features.include?(:oidc) %>
60
+ <% if params[:prompt] %>
61
+ <%= hidden_field_tag :prompt, params[:prompt] %>
62
+ <% end %>
54
63
  <% if params[:nonce] %>
55
64
  <%= hidden_field_tag :nonce, params[:nonce] %>
56
65
  <% end %>
@@ -60,13 +69,16 @@
60
69
  <% if params[:claims_locales] %>
61
70
  <%= hidden_field_tag :claims_locales, params[:claims_locales] %>
62
71
  <% end %>
72
+ <% if params[:claims] %>
73
+ <%= hidden_field_tag :claims, sanitize(params[:claims]) %>
74
+ <% end %>
63
75
  <% if params[:acr_values] %>
64
- <%= hidden_field_tag :acr, params[:acr_values] %>
76
+ <%= hidden_field_tag :acr_values, params[:acr_values] %>
65
77
  <% end %>
66
78
  <% end %>
67
79
  </div>
68
80
  <p class="text-center">
69
81
  <%= submit_tag rodauth.oauth_authorize_button, class: "btn btn-outline-primary" %>
70
- <%= link_to rodauth.oauth_cancel_button, "#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request#{"&state=\#{rodauth.state}" if params[:state] }", class: "btn btn-outline-danger" %>
82
+ <%= link_to rodauth.oauth_cancel_button, "#{rodauth.redirect_uri}?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request#{"&state=\#{CGI.escape(rodauth.state)}" if params[:state] }", class: "btn btn-outline-danger" %>
71
83
  </p>
72
84
  <% end %>
@@ -5,42 +5,48 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
5
5
  t.foreign_key :accounts, column: :account_id
6
6
  t.string :name, null: false
7
7
  t.string :description, null: true
8
- t.string :homepage_url, null: false
8
+ t.string :homepage_url, null: true
9
9
  t.string :redirect_uri, null: false
10
10
  t.string :client_id, null: false, index: { unique: true }
11
11
  t.string :client_secret, null: false, index: { unique: true }
12
12
  t.string :scopes, null: false
13
13
  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
14
- # extra params
15
- # t.string :token_endpoint_auth_method, null: true
16
- # t.string :grant_types, null: true
17
- # t.string :response_types, null: true
18
- # t.string :client_uri, null: true
19
- # t.string :logo_uri, null: true
20
- # t.string :tos_uri, null: true
21
- # t.string :policy_uri, null: true
22
- # t.string :jwks_uri, null: true
23
- # t.string :jwks, null: true
24
- # t.string :contacts, null: true
25
- # t.string :software_id, null: true
26
- # t.string :software_version, null: true
27
- # oidc extra params
28
- # t.string :sector_identifier_uri, null: true
29
- # t.string :application_type, null: true
30
- # t.string :subject_type, null: true
31
- # t.string :id_token_signed_response_alg, null: true
32
- # t.string :id_token_encrypted_response_alg, null: true
33
- # t.string :id_token_encrypted_response_enc, null: true
34
- # t.string :userinfo_signed_response_alg, null: true
35
- # t.string :userinfo_encrypted_response_alg, null: true
36
- # t.string :userinfo_encrypted_response_enc, null: true
37
- # t.string :request_object_signing_alg, null: true
38
- # t.string :request_object_encryption_alg, null: true
39
- # t.string :request_object_encryption_enc, null: true
40
- # JWT/OIDC per application signing verification
41
- # t.text :jwt_public_key, null: true
42
- # RP-initiated logout
43
- # t.string :post_logout_redirect_uri, null: false
14
+
15
+ # :oauth_dynamic_client_configuration enabled, extra optional params
16
+ t.string :token_endpoint_auth_method, null: true
17
+ t.string :grant_types, null: true
18
+ t.string :response_types, null: true
19
+ t.string :client_uri, null: true
20
+ t.string :logo_uri, null: true
21
+ t.string :tos_uri, null: true
22
+ t.string :policy_uri, null: true
23
+ t.string :jwks_uri, null: true
24
+ t.string :jwks, null: true
25
+ t.string :contacts, null: true
26
+ t.string :software_id, null: true
27
+ t.string :software_version, null: true
28
+
29
+ # :oidc_dynamic_client_configuration enabled, extra optional params
30
+ t.string :sector_identifier_uri, null: true
31
+ t.string :application_type, null: true
32
+
33
+ # :oidc enabled
34
+ t.string :subject_type, null: true
35
+ t.string :id_token_signed_response_alg, null: true
36
+ t.string :id_token_encrypted_response_alg, null: true
37
+ t.string :id_token_encrypted_response_enc, null: true
38
+ t.string :userinfo_signed_response_alg, null: true
39
+ t.string :userinfo_encrypted_response_alg, null: true
40
+ t.string :userinfo_encrypted_response_enc, null: true
41
+
42
+ # :oauth_jwt_secured_authorization_request
43
+ t.string :request_object_signing_alg, null: true
44
+ t.string :request_object_encryption_alg, null: true
45
+ t.string :request_object_encryption_enc, null: true
46
+ t.string :request_uris, null: true
47
+
48
+ # :oidc_rp_initiated_logout enabled
49
+ t.string :post_logout_redirect_uris, null: false
44
50
  end
45
51
 
46
52
  create_table :oauth_grants do |t|
@@ -58,19 +64,24 @@ class CreateRodauthOauth < ActiveRecord::Migration<%= migration_version %>
58
64
  t.datetime :revoked_at
59
65
  t.string :scopes, null: false
60
66
  t.datetime :created_at, null: false, default: -> { "CURRENT_TIMESTAMP" }
61
- # for using access_types
62
67
  t.string :access_type, null: false, default: "offline"
63
- # uncomment to enable PKCE
64
- # t.string :code_challenge
65
- # t.string :code_challenge_method
66
- # device code grant
67
- # t.string :user_code, null: true, unique: true
68
- # t.datetime :last_polled_at, null: true
69
- # when using :oauth_resource_indicators feature
70
- # t.string :resource
71
- # uncomment to use OIDC nonce
72
- # t.string :nonce
73
- # t.string :acr
68
+
69
+ # :oauth_pkce enabled
70
+ t.string :code_challenge
71
+ t.string :code_challenge_method
72
+
73
+ # :oauth_device_code_grant enabled
74
+ t.string :user_code, null: true, unique: true
75
+ t.datetime :last_polled_at, null: true
76
+
77
+ # :resource_indicators enabled
78
+ t.string :resource
79
+
80
+ # :oidc enabled
81
+ t.string :nonce
82
+ t.string :acr
83
+ t.string :claims_locales
84
+ t.string :claims
74
85
  end
75
86
  end
76
87
  end
@@ -165,10 +165,10 @@ module Rodauth
165
165
  value.each do |uri|
166
166
  next if uri.empty?
167
167
 
168
- set_field_error(key, invalid_url_message) unless check_valid_uri?(uri)
168
+ set_field_error(key, invalid_url_message) unless check_valid_no_fragment_uri?(uri)
169
169
  end
170
170
  else
171
- set_field_error(key, invalid_url_message) unless check_valid_uri?(value)
171
+ set_field_error(key, invalid_url_message) unless check_valid_no_fragment_uri?(value)
172
172
  end
173
173
  elsif key == oauth_application_scopes_param
174
174
 
@@ -25,9 +25,9 @@ module Rodauth
25
25
  def validate_authorize_params
26
26
  super
27
27
 
28
- return unless (response_mode = param_or_nil("response_mode")) && !oauth_response_modes_supported.include?(response_mode)
28
+ response_mode = param_or_nil("response_mode")
29
29
 
30
- redirect_response_error("invalid_request")
30
+ redirect_response_error("invalid_request") if response_mode && !oauth_response_modes_supported.include?(response_mode)
31
31
  end
32
32
 
33
33
  def validate_token_params
@@ -46,7 +46,6 @@ module Rodauth
46
46
 
47
47
  case response_type
48
48
  when "code", nil
49
- response_mode ||= oauth_response_mode
50
49
  response_params.replace(_do_authorize_code)
51
50
  end
52
51
 
@@ -68,7 +67,7 @@ module Rodauth
68
67
  redirect_url = URI.parse(redirect_uri)
69
68
  case mode
70
69
  when "query"
71
- params = params.map { |k, v| "#{k}=#{v}" }
70
+ params = params.map { |k, v| "#{CGI.escape(k)}=#{CGI.escape(v)}" }
72
71
  params << redirect_url.query if redirect_url.query
73
72
  redirect_url.query = params.join("&")
74
73
  redirect(redirect_url.to_s)
@@ -80,7 +79,7 @@ module Rodauth
80
79
  <form method="post" action="#{redirect_uri}">
81
80
  #{
82
81
  params.map do |name, value|
83
- "<input type=\"hidden\" name=\"#{name}\" value=\"#{scope.h(value)}\" />"
82
+ "<input type=\"hidden\" name=\"#{scope.h(name)}\" value=\"#{scope.h(value)}\" />"
84
83
  end.join
85
84
  }
86
85
  <input type="submit" class="btn btn-outline-primary" value="#{scope.h(oauth_authorize_post_button)}"/>
@@ -91,6 +90,32 @@ module Rodauth
91
90
  end
92
91
  end
93
92
 
93
+ def _redirect_response_error(redirect_url, query_params)
94
+ response_mode = param_or_nil("response_mode") || oauth_response_mode
95
+
96
+ case response_mode
97
+ when "form_post"
98
+ 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
113
+ request.halt
114
+ else
115
+ super
116
+ end
117
+ end
118
+
94
119
  def create_token(grant_type)
95
120
  return super unless supported_grant_type?(grant_type, "authorization_code")
96
121
 
@@ -107,7 +132,7 @@ module Rodauth
107
132
  def check_valid_response_type?
108
133
  response_type = param_or_nil("response_type")
109
134
 
110
- response_type.nil? || response_type == "code" || response_type == "none" || super
135
+ response_type == "code" || response_type == "none" || super
111
136
  end
112
137
 
113
138
  def oauth_server_metadata_body(*)
@@ -23,6 +23,8 @@ module Rodauth
23
23
  translatable_method :oauth_applications_contacts_label, "Contacts"
24
24
  translatable_method :oauth_applications_tos_uri_label, "Terms of service URL"
25
25
  translatable_method :oauth_applications_policy_uri_label, "Policy URL"
26
+ translatable_method :oauth_unsupported_response_type_message, "Unsupported response type"
27
+ translatable_method :oauth_authorize_parameter_required, "'%<parameter>s' is a required parameter"
26
28
 
27
29
  # /authorize
28
30
  auth_server_route(:authorize) do |r|
@@ -65,7 +67,7 @@ module Rodauth
65
67
  def validate_authorize_params
66
68
  redirect_response_error("invalid_request", request.referer || default_redirect) unless oauth_application && check_valid_redirect_uri?
67
69
 
68
- redirect_response_error("invalid_request") unless check_valid_response_type?
70
+ redirect_response_error("unsupported_response_type") unless check_valid_response_type?
69
71
 
70
72
  redirect_response_error("invalid_request") unless check_valid_access_type? && check_valid_approval_prompt?
71
73
 
@@ -79,7 +81,15 @@ module Rodauth
79
81
  end
80
82
 
81
83
  def check_valid_redirect_uri?
82
- oauth_application[oauth_applications_redirect_uri_column].split(" ").include?(redirect_uri)
84
+ application_redirect_uris = oauth_application[oauth_applications_redirect_uri_column].split(" ")
85
+
86
+ if (redirect_uri = param_or_nil("redirect_uri"))
87
+ application_redirect_uris.include?(redirect_uri)
88
+ else
89
+ set_error_flash(oauth_authorize_parameter_required(parameter: "redirect_uri")) if application_redirect_uris.size > 1
90
+
91
+ true
92
+ end
83
93
  end
84
94
 
85
95
  ACCESS_TYPES = %w[offline online].freeze
@@ -140,10 +150,10 @@ module Rodauth
140
150
  end
141
151
 
142
152
  def create_oauth_grant(create_params = {})
143
- create_params[oauth_grants_oauth_application_id_column] = oauth_application[oauth_applications_id_column]
144
- create_params[oauth_grants_redirect_uri_column] = redirect_uri
145
- create_params[oauth_grants_expires_in_column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in)
146
- create_params[oauth_grants_scopes_column] = scopes.join(oauth_scope_separator)
153
+ create_params[oauth_grants_oauth_application_id_column] ||= oauth_application[oauth_applications_id_column]
154
+ create_params[oauth_grants_redirect_uri_column] ||= redirect_uri
155
+ create_params[oauth_grants_expires_in_column] ||= Sequel.date_add(Sequel::CURRENT_TIMESTAMP, seconds: oauth_grant_expires_in)
156
+ create_params[oauth_grants_scopes_column] ||= scopes.join(oauth_scope_separator)
147
157
 
148
158
  if use_oauth_access_type? && (access_type = param_or_nil("access_type"))
149
159
  create_params[oauth_grants_access_type_column] = access_type
@@ -3,6 +3,7 @@
3
3
  require "time"
4
4
  require "base64"
5
5
  require "securerandom"
6
+ require "cgi"
6
7
  require "rodauth/version"
7
8
  require "rodauth/oauth"
8
9
  require "rodauth/oauth/database_extensions"
@@ -17,8 +18,6 @@ module Rodauth
17
18
  auth_value_methods(:http_request)
18
19
  auth_value_methods(:http_request_cache)
19
20
 
20
- SCOPES = %w[profile.read].freeze
21
-
22
21
  before "token"
23
22
 
24
23
  error_flash "Please authorize to continue", "require_authorization"
@@ -82,7 +81,7 @@ module Rodauth
82
81
  auth_value_method :oauth_already_in_use_response_status, 409
83
82
 
84
83
  # Feature options
85
- auth_value_method :oauth_application_scopes, SCOPES
84
+ auth_value_method :oauth_application_scopes, []
86
85
  auth_value_method :oauth_token_type, "bearer"
87
86
  auth_value_method :oauth_refresh_token_protection_policy, "rotation" # can be: none, sender_constrained, rotation
88
87
 
@@ -228,13 +227,20 @@ module Rodauth
228
227
  end
229
228
 
230
229
  def fetch_access_token
231
- value = request.env["HTTP_AUTHORIZATION"]
230
+ if (token = request.params["access_token"])
231
+ if request.post? && !(request.content_type.start_with?("application/x-www-form-urlencoded") &&
232
+ request.params.size == 1)
233
+ return
234
+ end
235
+ else
236
+ value = request.env["HTTP_AUTHORIZATION"]
232
237
 
233
- return unless value && !value.empty?
238
+ return unless value && !value.empty?
234
239
 
235
- scheme, token = value.split(" ", 2)
240
+ scheme, token = value.split(" ", 2)
236
241
 
237
- return unless scheme.downcase == oauth_token_type
242
+ return unless scheme.downcase == oauth_token_type
243
+ end
238
244
 
239
245
  return if token.nil? || token.empty?
240
246
 
@@ -245,11 +251,11 @@ module Rodauth
245
251
  return @authorization_token if defined?(@authorization_token)
246
252
 
247
253
  # check if there is a token
248
- bearer_token = fetch_access_token
254
+ access_token = fetch_access_token
249
255
 
250
- return unless bearer_token
256
+ return unless access_token
251
257
 
252
- @authorization_token = oauth_grant_by_token(bearer_token)
258
+ @authorization_token = oauth_grant_by_token(access_token)
253
259
  end
254
260
 
255
261
  def require_oauth_authorization(*scopes)
@@ -758,22 +764,31 @@ module Rodauth
758
764
  query_params = []
759
765
 
760
766
  query_params << if respond_to?(:"oauth_#{error_code}_error_code")
761
- "error=#{send(:"oauth_#{error_code}_error_code")}"
767
+ ["error", send(:"oauth_#{error_code}_error_code")]
762
768
  else
763
- "error=#{error_code}"
769
+ ["error", error_code]
764
770
  end
765
771
 
766
772
  if respond_to?(:"oauth_#{error_code}_message")
767
773
  message = send(:"oauth_#{error_code}_message")
768
- query_params << ["error_description=#{CGI.escape(message)}"]
774
+ query_params << ["error_description", CGI.escape(message)]
769
775
  end
770
776
 
771
- query_params << redirect_url.query if redirect_url.query
772
- redirect_url.query = query_params.join("&")
773
- redirect(redirect_url.to_s)
777
+ state = param_or_nil("state")
778
+
779
+ query_params << ["state", state] if state
780
+
781
+ _redirect_response_error(redirect_url, query_params)
774
782
  end
775
783
  end
776
784
 
785
+ def _redirect_response_error(redirect_url, query_params)
786
+ query_params = query_params.map { |k, v| "#{k}=#{v}" }
787
+ query_params << redirect_url.query if redirect_url.query
788
+ redirect_url.query = query_params.join("&")
789
+ redirect(redirect_url.to_s)
790
+ end
791
+
777
792
  def json_response_success(body, cache = false)
778
793
  response.status = 200
779
794
  response["Content-Type"] ||= json_response_content_type
@@ -835,6 +850,10 @@ module Rodauth
835
850
  URI::DEFAULT_PARSER.make_regexp(oauth_valid_uri_schemes).match?(uri)
836
851
  end
837
852
 
853
+ def check_valid_no_fragment_uri?(uri)
854
+ check_valid_uri?(uri) && URI.parse(uri).fragment.nil?
855
+ end
856
+
838
857
  # Resource server mode
839
858
 
840
859
  def authorization_server_metadata
@@ -8,7 +8,7 @@ module Rodauth
8
8
 
9
9
  before "register"
10
10
 
11
- auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name client_uri]
11
+ auth_value_method :oauth_client_registration_required_params, %w[redirect_uris client_name]
12
12
 
13
13
  PROTECTED_APPLICATION_ATTRIBUTES = %w[account_id client_id].freeze
14
14
 
@@ -68,7 +68,10 @@ module Rodauth
68
68
  when "redirect_uris"
69
69
  if value.is_a?(Array)
70
70
  value = value.each do |uri|
71
- register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(uri)) unless check_valid_uri?(uri)
71
+ unless check_valid_no_fragment_uri?(uri)
72
+ register_throw_json_response_error("invalid_redirect_uri",
73
+ register_invalid_uri_message(uri))
74
+ end
72
75
  end.join(" ")
73
76
  else
74
77
  register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
@@ -76,7 +79,7 @@ module Rodauth
76
79
  key = oauth_applications_redirect_uri_column
77
80
  when "token_endpoint_auth_method"
78
81
  unless oauth_token_endpoint_auth_methods_supported.include?(value)
79
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message(key))
82
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(key, value))
80
83
  end
81
84
  # verify if in range
82
85
  key = oauth_applications_token_endpoint_auth_method_column
@@ -84,7 +87,7 @@ module Rodauth
84
87
  if value.is_a?(Array)
85
88
  value = value.each do |grant_type|
86
89
  unless oauth_grant_types_supported.include?(grant_type)
87
- register_throw_json_response_error("invalid_client_metadata", register_oauth_invalid_grant_type_message(grant_type))
90
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message(grant_type, value))
88
91
  end
89
92
  end.join(" ")
90
93
  else
@@ -28,23 +28,24 @@ module Rodauth
28
28
 
29
29
  redirect_response_error("invalid_request") unless supported_response_mode?(response_mode)
30
30
 
31
- response_params.replace(_do_authorize_token)
31
+ oauth_grant = _do_authorize_token
32
+
33
+ response_params.replace(json_access_token_payload(oauth_grant))
32
34
 
33
35
  response_params["state"] = param("state") if param_or_nil("state")
34
36
 
35
37
  [response_params, response_mode]
36
38
  end
37
39
 
38
- def _do_authorize_token
40
+ def _do_authorize_token(grant_params = {})
39
41
  grant_params = {
40
42
  oauth_grants_type_column => "implicit",
41
43
  oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
42
44
  oauth_grants_scopes_column => scopes,
43
45
  oauth_grants_account_id_column => account_id
44
- }
45
- oauth_grant = generate_token(grant_params, false)
46
+ }.merge(grant_params)
46
47
 
47
- json_access_token_payload(oauth_grant)
48
+ generate_token(grant_params, false)
48
49
  end
49
50
 
50
51
  def authorize_response(params, mode)
@@ -49,11 +49,11 @@ module Rodauth
49
49
  return @authorization_token if defined?(@authorization_token)
50
50
 
51
51
  @authorization_token = begin
52
- bearer_token = fetch_access_token
52
+ access_token = fetch_access_token
53
53
 
54
- return unless bearer_token
54
+ return unless access_token
55
55
 
56
- jwt_claims = jwt_decode(bearer_token)
56
+ jwt_claims = jwt_decode(access_token)
57
57
 
58
58
  return unless jwt_claims
59
59
 
@@ -63,7 +63,11 @@ module Rodauth
63
63
  end
64
64
 
65
65
  def jwt_subject(oauth_grant, client_application = oauth_application)
66
- oauth_grant[oauth_grants_account_id_column] || client_application[oauth_applications_client_id_column]
66
+ account_id = oauth_grant[oauth_grants_account_id_column]
67
+
68
+ return account_id.to_s if account_id
69
+
70
+ client_application[oauth_applications_client_id_column]
67
71
  end
68
72
 
69
73
  def oauth_server_metadata_body(path = nil)
@@ -207,14 +211,23 @@ module Rodauth
207
211
 
208
212
  claims = if is_authorization_server?
209
213
  if jwks
214
+ jwks = jwks[:keys] if jwks.is_a?(Hash)
215
+
210
216
  enc_algs = [jws_encryption_algorithm].compact
211
217
  enc_meths = [jws_encryption_method].compact
212
218
 
213
219
  sig_algs = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
214
220
  sig_algs = sig_algs.compact.map(&:to_sym)
215
221
 
216
- jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
217
- jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
222
+ # JWKs may be set up without a KID, when there's a single one
223
+ if jwks.size == 1 && !jwks[0][:kid]
224
+ key = jwks[0]
225
+ jwk_key = JSON::JWK.new(key)
226
+ jws = JSON::JWT.decode(token, jwk_key)
227
+ else
228
+ jws = JSON::JWT.decode(token, JSON::JWK::Set.new({ keys: jwks }), enc_algs + sig_algs, enc_meths)
229
+ jws = JSON::JWT.decode(jws.plain_text, JSON::JWK::Set.new({ keys: jwks }), sig_algs) if jws.is_a?(JSON::JWE)
230
+ end
218
231
  jws
219
232
  elsif jws_key
220
233
  JSON::JWT.decode(token, jws_key)
@@ -279,7 +292,7 @@ module Rodauth
279
292
  end
280
293
 
281
294
  def jwt_encode(payload,
282
- signing_algorithm: oauth_jwt_keys.keys.first)
295
+ signing_algorithm: oauth_jwt_keys.keys.first, **)
283
296
  headers = {}
284
297
 
285
298
  key = oauth_jwt_keys[signing_algorithm] || _jwt_key
@@ -368,8 +381,18 @@ module Rodauth
368
381
  # decode jwt
369
382
  claims = if is_authorization_server?
370
383
  if jwks
371
- algorithms = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
372
- JWT.decode(token, nil, true, algorithms: algorithms, jwks: { keys: jwks }, **verify_claims_params).first
384
+ jwks = jwks[:keys] if jwks.is_a?(Hash)
385
+
386
+ # JWKs may be set up without a KID, when there's a single one
387
+ if jwks.size == 1 && !jwks[0][:kid]
388
+ key = jwks[0]
389
+ algo = key[:alg]
390
+ key = JWT::JWK.import(key).keypair
391
+ JWT.decode(token, key, true, algorithms: [algo], **verify_claims_params).first
392
+ else
393
+ algorithms = jws_algorithm ? [jws_algorithm] : jwks.select { |k| k[:use] == "sig" }.map { |k| k[:alg] }
394
+ JWT.decode(token, nil, true, algorithms: algorithms, jwks: { keys: jwks }, **verify_claims_params).first
395
+ end
373
396
  elsif jws_key
374
397
  JWT.decode(token, jws_key, true, algorithms: [jws_algorithm], **verify_claims_params).first
375
398
  end