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 +4 -4
- data/README.md +6 -5
- data/doc/release_notes/1_0_0_beta2.md +34 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +19 -7
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +54 -43
- data/lib/rodauth/features/oauth_application_management.rb +2 -2
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +31 -6
- data/lib/rodauth/features/oauth_authorize_base.rb +16 -6
- data/lib/rodauth/features/oauth_base.rb +35 -16
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +7 -4
- data/lib/rodauth/features/oauth_implicit_grant.rb +6 -5
- data/lib/rodauth/features/oauth_jwt.rb +3 -3
- data/lib/rodauth/features/oauth_jwt_base.rb +29 -6
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +7 -4
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +64 -10
- data/lib/rodauth/features/oauth_resource_indicators.rb +0 -4
- data/lib/rodauth/features/oauth_resource_server.rb +3 -3
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +2 -0
- data/lib/rodauth/features/oidc.rb +231 -183
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +65 -25
- data/lib/rodauth/features/oidc_rp_initiated_logout.rb +115 -0
- data/lib/rodauth/oauth/http_extensions.rb +15 -2
- data/lib/rodauth/oauth/ttl_store.rb +2 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/locales/en.yml +3 -1
- data/locales/pt.yml +3 -1
- data/templates/authorize.str +17 -10
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f96bced835f21567ea5603751e8cf06b53d4eae70d9cab8bc685e2fca8c2027
|
4
|
+
data.tar.gz: 59badde6a055fa2638bcb41b01534f0b5b8de9eac541ac596f25e6d5cd6fb043
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
*
|
39
|
-
* [OpenID Connect
|
40
|
-
* [OpenID
|
41
|
-
* [OpenID
|
42
|
-
* [
|
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
|
-
|
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
|
-
|
30
|
-
<%=
|
31
|
-
|
32
|
-
|
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 :
|
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:
|
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
|
-
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
#
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
#
|
43
|
-
|
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
|
-
|
64
|
-
#
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
#
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
#
|
73
|
-
|
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
|
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
|
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
|
-
|
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
|
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("
|
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(" ")
|
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]
|
144
|
-
create_params[oauth_grants_redirect_uri_column]
|
145
|
-
create_params[oauth_grants_expires_in_column]
|
146
|
-
create_params[oauth_grants_scopes_column]
|
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,
|
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
|
-
|
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
|
-
|
238
|
+
return unless value && !value.empty?
|
234
239
|
|
235
|
-
|
240
|
+
scheme, token = value.split(" ", 2)
|
236
241
|
|
237
|
-
|
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
|
-
|
254
|
+
access_token = fetch_access_token
|
249
255
|
|
250
|
-
return unless
|
256
|
+
return unless access_token
|
251
257
|
|
252
|
-
@authorization_token = oauth_grant_by_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
|
767
|
+
["error", send(:"oauth_#{error_code}_error_code")]
|
762
768
|
else
|
763
|
-
"error
|
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
|
774
|
+
query_params << ["error_description", CGI.escape(message)]
|
769
775
|
end
|
770
776
|
|
771
|
-
|
772
|
-
|
773
|
-
|
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
|
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
|
-
|
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",
|
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",
|
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
|
-
|
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
|
-
|
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
|
-
|
52
|
+
access_token = fetch_access_token
|
53
53
|
|
54
|
-
return unless
|
54
|
+
return unless access_token
|
55
55
|
|
56
|
-
jwt_claims = jwt_decode(
|
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]
|
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
|
-
|
217
|
-
|
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
|
-
|
372
|
-
|
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
|