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

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: 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