rodauth-oauth 0.10.4 → 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/MIGRATION-GUIDE-v1.md +286 -0
- data/README.md +28 -35
- data/doc/release_notes/1_0_0_beta1.md +38 -0
- data/doc/release_notes/1_0_0_beta2.md +34 -0
- data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +21 -11
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
- data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +57 -57
- data/lib/rodauth/features/oauth_application_management.rb +61 -74
- data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
- data/lib/rodauth/features/oauth_authorization_code_grant.rb +62 -90
- data/lib/rodauth/features/oauth_authorize_base.rb +115 -22
- data/lib/rodauth/features/oauth_base.rb +397 -315
- data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
- data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
- data/lib/rodauth/features/oauth_dynamic_client_registration.rb +52 -31
- data/lib/rodauth/features/oauth_grant_management.rb +70 -0
- data/lib/rodauth/features/oauth_implicit_grant.rb +29 -27
- data/lib/rodauth/features/oauth_jwt.rb +53 -689
- data/lib/rodauth/features/oauth_jwt_base.rb +458 -0
- data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +48 -17
- data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
- data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +116 -0
- data/lib/rodauth/features/oauth_management_base.rb +2 -0
- data/lib/rodauth/features/oauth_pkce.rb +22 -26
- data/lib/rodauth/features/oauth_resource_indicators.rb +33 -25
- data/lib/rodauth/features/oauth_resource_server.rb +59 -0
- data/lib/rodauth/features/oauth_saml_bearer_grant.rb +7 -1
- data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
- data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
- data/lib/rodauth/features/oidc.rb +382 -241
- data/lib/rodauth/features/oidc_dynamic_client_registration.rb +127 -51
- data/lib/rodauth/features/oidc_rp_initiated_logout.rb +115 -0
- data/lib/rodauth/oauth/database_extensions.rb +8 -6
- data/lib/rodauth/oauth/http_extensions.rb +74 -0
- data/lib/rodauth/oauth/railtie.rb +20 -0
- data/lib/rodauth/oauth/ttl_store.rb +2 -0
- data/lib/rodauth/oauth/version.rb +1 -1
- data/lib/rodauth/oauth.rb +29 -1
- data/locales/en.yml +34 -22
- data/locales/pt.yml +34 -22
- data/templates/authorize.str +19 -17
- data/templates/device_search.str +1 -1
- data/templates/device_verification.str +2 -2
- data/templates/jwks_field.str +1 -0
- data/templates/new_oauth_application.str +1 -2
- data/templates/oauth_application.str +2 -2
- data/templates/oauth_application_oauth_grants.str +54 -0
- data/templates/oauth_applications.str +2 -2
- data/templates/oauth_grants.str +52 -0
- metadata +23 -16
- data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
- data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
- data/lib/rodauth/features/oauth.rb +0 -9
- data/lib/rodauth/features/oauth_http_mac.rb +0 -86
- data/lib/rodauth/features/oauth_token_management.rb +0 -81
- data/lib/rodauth/oauth/refinements.rb +0 -48
- data/templates/jwt_public_key_field.str +0 -4
- data/templates/oauth_application_oauth_tokens.str +0 -52
- data/templates/oauth_tokens.str +0 -50
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
3
5
|
module Rodauth
|
4
6
|
Feature.define(:oidc_dynamic_client_registration, :OidcDynamicClientRegistration) do
|
5
7
|
depends :oauth_dynamic_client_registration, :oidc
|
@@ -8,10 +10,6 @@ module Rodauth
|
|
8
10
|
|
9
11
|
private
|
10
12
|
|
11
|
-
def registration_metadata
|
12
|
-
openid_configuration_body
|
13
|
-
end
|
14
|
-
|
15
13
|
def validate_client_registration_params
|
16
14
|
super
|
17
15
|
|
@@ -43,11 +41,57 @@ module Rodauth
|
|
43
41
|
else
|
44
42
|
register_throw_json_response_error("invalid_client_metadata", register_invalid_application_type_message(type))
|
45
43
|
end
|
46
|
-
|
44
|
+
end
|
45
|
+
|
46
|
+
if (value = @oauth_application_params[oauth_applications_sector_identifier_uri_column])
|
47
|
+
uri = URI(value)
|
48
|
+
|
49
|
+
unless uri.scheme == "https" || uri.host == "localhost"
|
50
|
+
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(uri))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
if features.include?(:oauth_jwt_secured_authorization_request)
|
55
|
+
if (value = @oauth_application_params[oauth_applications_request_uris_column])
|
56
|
+
if value.is_a?(Array)
|
57
|
+
@oauth_application_params[oauth_applications_request_uris_column] = value.each do |req_uri|
|
58
|
+
unless check_valid_uri?(req_uri)
|
59
|
+
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(req_uri))
|
60
|
+
end
|
61
|
+
end.join(" ")
|
62
|
+
else
|
63
|
+
register_throw_json_response_error("invalid_redirect_uri", register_invalid_uri_message(value))
|
64
|
+
end
|
65
|
+
elsif oauth_require_request_uri_registration
|
66
|
+
register_throw_json_response_error("invalid_client_metadata", register_required_param_message("request_uris"))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
if (value = @oauth_application_params[oauth_applications_subject_type_column])
|
47
71
|
unless %w[pairwise public].include?(value)
|
48
|
-
register_throw_json_response_error("invalid_client_metadata",
|
72
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message("subject_type", value))
|
49
73
|
end
|
50
|
-
|
74
|
+
|
75
|
+
if value == "pairwise"
|
76
|
+
sector_identifier_uri = @oauth_application_params[oauth_applications_sector_identifier_uri_column]
|
77
|
+
|
78
|
+
if sector_identifier_uri
|
79
|
+
response = http_request(sector_identifier_uri)
|
80
|
+
unless response.code.to_i == 200
|
81
|
+
register_throw_json_response_error("invalid_client_metadata",
|
82
|
+
register_invalid_param_message("sector_identifier_uri"))
|
83
|
+
end
|
84
|
+
uris = JSON.parse(response.body)
|
85
|
+
|
86
|
+
if uris != @oauth_application_params[oauth_applications_redirect_uri_column].split(" ")
|
87
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("sector_identifier_uri"))
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
if (value = @oauth_application_params[oauth_applications_id_token_signed_response_alg_column])
|
51
95
|
if value == "none"
|
52
96
|
# The value none MUST NOT be used as the ID Token alg value unless the Client uses only Response Types
|
53
97
|
# that return no ID Token from the Authorization Endpoint
|
@@ -55,42 +99,77 @@ module Rodauth
|
|
55
99
|
if response_types && response_types.include?("id_token")
|
56
100
|
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_signed_response_alg"))
|
57
101
|
end
|
58
|
-
elsif !
|
59
|
-
register_throw_json_response_error("invalid_client_metadata",
|
60
|
-
|
61
|
-
elsif (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_alg_column])
|
62
|
-
unless oauth_jwt_jwe_algorithms_supported.include?(value)
|
63
|
-
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_encrypted_response_alg"))
|
64
|
-
end
|
65
|
-
elsif (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_enc_column])
|
66
|
-
unless oauth_jwt_jwe_encryption_methods_supported.include?(value)
|
67
|
-
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_encrypted_response_enc"))
|
68
|
-
end
|
69
|
-
elsif (value = @oauth_application_params[oauth_applications_userinfo_signed_response_alg_column])
|
70
|
-
unless oauth_jwt_algorithms_supported.include?(value)
|
71
|
-
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("userinfo_signed_response_alg"))
|
72
|
-
end
|
73
|
-
elsif (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_alg_column])
|
74
|
-
unless oauth_jwt_jwe_algorithms_supported.include?(value)
|
75
|
-
register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("userinfo_encrypted_response_alg"))
|
102
|
+
elsif !oauth_jwt_jws_algorithms_supported.include?(value)
|
103
|
+
register_throw_json_response_error("invalid_client_metadata",
|
104
|
+
register_invalid_client_metadata_message("id_token_signed_response_alg", value))
|
76
105
|
end
|
77
|
-
|
78
|
-
|
79
|
-
|
106
|
+
end
|
107
|
+
|
108
|
+
if features.include?(:oauth_jwt_secured_authorization_request)
|
109
|
+
if defined?(oauth_applications_request_object_signing_alg_column) &&
|
110
|
+
(value = @oauth_application_params[oauth_applications_request_object_signing_alg_column]) &&
|
111
|
+
!oauth_jwt_jws_algorithms_supported.include?(value) && !(value == "none" && oauth_request_object_signing_alg_allow_none)
|
112
|
+
register_throw_json_response_error("invalid_client_metadata",
|
113
|
+
register_invalid_client_metadata_message("request_object_signing_alg", value))
|
80
114
|
end
|
81
|
-
|
82
|
-
|
83
|
-
|
115
|
+
|
116
|
+
if defined?(oauth_applications_request_object_encryption_alg_column) &&
|
117
|
+
(value = @oauth_application_params[oauth_applications_request_object_encryption_alg_column]) &&
|
118
|
+
!oauth_jwt_jwe_algorithms_supported.include?(value)
|
119
|
+
register_throw_json_response_error("invalid_client_metadata",
|
120
|
+
register_invalid_client_metadata_message("request_object_encryption_alg", value))
|
84
121
|
end
|
85
|
-
|
86
|
-
|
87
|
-
|
122
|
+
|
123
|
+
if defined?(oauth_applications_request_object_encryption_enc_column) &&
|
124
|
+
(value = @oauth_application_params[oauth_applications_request_object_encryption_enc_column]) &&
|
125
|
+
!oauth_jwt_jwe_encryption_methods_supported.include?(value)
|
126
|
+
register_throw_json_response_error("invalid_client_metadata",
|
127
|
+
register_invalid_client_metadata_message("request_object_encryption_enc", value))
|
88
128
|
end
|
89
|
-
|
90
|
-
|
91
|
-
|
129
|
+
end
|
130
|
+
|
131
|
+
if features.include?(:oidc_rp_initiated_logout) && (defined?(oauth_applications_post_logout_redirect_uris_column) &&
|
132
|
+
(value = @oauth_application_params[oauth_applications_post_logout_redirect_uris_column]))
|
133
|
+
if value.is_a?(Array)
|
134
|
+
@oauth_application_params[oauth_applications_post_logout_redirect_uris_column] = value.each do |redirect_uri|
|
135
|
+
unless check_valid_uri?(redirect_uri)
|
136
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(redirect_uri))
|
137
|
+
end
|
138
|
+
end.join(" ")
|
139
|
+
else
|
140
|
+
register_throw_json_response_error("invalid_client_metadata", register_invalid_uri_message(value))
|
92
141
|
end
|
93
142
|
end
|
143
|
+
|
144
|
+
if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_alg_column]) &&
|
145
|
+
!oauth_jwt_jwe_algorithms_supported.include?(value)
|
146
|
+
register_throw_json_response_error("invalid_client_metadata",
|
147
|
+
register_invalid_client_metadata_message("id_token_encrypted_response_alg", value))
|
148
|
+
end
|
149
|
+
|
150
|
+
if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_enc_column]) &&
|
151
|
+
!oauth_jwt_jwe_encryption_methods_supported.include?(value)
|
152
|
+
register_throw_json_response_error("invalid_client_metadata",
|
153
|
+
register_invalid_client_metadata_message("id_token_encrypted_response_enc", value))
|
154
|
+
end
|
155
|
+
|
156
|
+
if (value = @oauth_application_params[oauth_applications_userinfo_signed_response_alg_column]) &&
|
157
|
+
!oauth_jwt_jws_algorithms_supported.include?(value)
|
158
|
+
register_throw_json_response_error("invalid_client_metadata",
|
159
|
+
register_invalid_client_metadata_message("userinfo_signed_response_alg", value))
|
160
|
+
end
|
161
|
+
|
162
|
+
if (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_alg_column]) &&
|
163
|
+
!oauth_jwt_jwe_algorithms_supported.include?(value)
|
164
|
+
register_throw_json_response_error("invalid_client_metadata",
|
165
|
+
register_invalid_client_metadata_message("userinfo_encrypted_response_alg", value))
|
166
|
+
end
|
167
|
+
|
168
|
+
if (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_enc_column]) &&
|
169
|
+
!oauth_jwt_jwe_encryption_methods_supported.include?(value)
|
170
|
+
register_throw_json_response_error("invalid_client_metadata",
|
171
|
+
register_invalid_client_metadata_message("userinfo_encrypted_response_enc", value))
|
172
|
+
end
|
94
173
|
end
|
95
174
|
|
96
175
|
def validate_client_registration_response_type(response_type, grant_types)
|
@@ -114,27 +193,24 @@ module Rodauth
|
|
114
193
|
return_params["application_type"] = "web"
|
115
194
|
"web"
|
116
195
|
end
|
117
|
-
create_params[oauth_applications_id_token_signed_response_alg_column] ||=
|
118
|
-
|
119
|
-
|
120
|
-
end
|
196
|
+
create_params[oauth_applications_id_token_signed_response_alg_column] ||= return_params["id_token_signed_response_alg"] =
|
197
|
+
oauth_jwt_keys.keys.first
|
198
|
+
|
121
199
|
if create_params.key?(oauth_applications_id_token_encrypted_response_alg_column)
|
122
|
-
create_params[oauth_applications_id_token_encrypted_response_enc_column] ||=
|
123
|
-
return_params["id_token_encrypted_response_enc"] = "A128CBC-HS256"
|
200
|
+
create_params[oauth_applications_id_token_encrypted_response_enc_column] ||= return_params["id_token_encrypted_response_enc"] =
|
124
201
|
"A128CBC-HS256"
|
125
|
-
|
202
|
+
|
126
203
|
end
|
127
204
|
if create_params.key?(oauth_applications_userinfo_encrypted_response_alg_column)
|
128
|
-
create_params[oauth_applications_userinfo_encrypted_response_enc_column] ||=
|
129
|
-
return_params["userinfo_encrypted_response_enc"] = "A128CBC-HS256"
|
205
|
+
create_params[oauth_applications_userinfo_encrypted_response_enc_column] ||= return_params["userinfo_encrypted_response_enc"] =
|
130
206
|
"A128CBC-HS256"
|
131
|
-
|
207
|
+
|
132
208
|
end
|
133
|
-
if
|
134
|
-
|
135
|
-
|
209
|
+
if defined?(oauth_applications_request_object_encryption_alg_column) &&
|
210
|
+
create_params.key?(oauth_applications_request_object_encryption_alg_column)
|
211
|
+
create_params[oauth_applications_request_object_encryption_enc_column] ||= return_params["request_object_encryption_enc"] =
|
136
212
|
"A128CBC-HS256"
|
137
|
-
|
213
|
+
|
138
214
|
end
|
139
215
|
|
140
216
|
super(return_params)
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rodauth/oauth"
|
4
|
+
|
5
|
+
module Rodauth
|
6
|
+
Feature.define(:oidc_rp_initiated_logout, :OidcRpInitiatedLogout) do
|
7
|
+
depends :oidc
|
8
|
+
|
9
|
+
auth_value_method :oauth_applications_post_logout_redirect_uris_column, :post_logout_redirect_uris
|
10
|
+
translatable_method :oauth_invalid_post_logout_redirect_uri_message, "Invalid post logout redirect URI"
|
11
|
+
|
12
|
+
# /oidc-logout
|
13
|
+
auth_server_route(:oidc_logout) do |r|
|
14
|
+
require_authorizable_account
|
15
|
+
before_oidc_logout_route
|
16
|
+
|
17
|
+
# OpenID Providers MUST support the use of the HTTP GET and POST methods
|
18
|
+
r.on method: %i[get post] do
|
19
|
+
catch_error do
|
20
|
+
validate_oidc_logout_params
|
21
|
+
|
22
|
+
#
|
23
|
+
# why this is done:
|
24
|
+
#
|
25
|
+
# we need to decode the id token in order to get the application, because, if the
|
26
|
+
# signing key is application-specific, we don't know how to verify the signature
|
27
|
+
# beforehand. Hence, we have to do it twice: decode-and-do-not-verify, initialize
|
28
|
+
# the @oauth_application, and then decode-and-verify.
|
29
|
+
#
|
30
|
+
claims = jwt_decode(param("id_token_hint"), verify_claims: false)
|
31
|
+
|
32
|
+
redirect_logout_with_error(oauth_invalid_client_message) unless claims
|
33
|
+
|
34
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["aud"]).first
|
35
|
+
oauth_grant = db[oauth_grants_table]
|
36
|
+
.where(
|
37
|
+
oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column],
|
38
|
+
oauth_grants_account_id_column => account_id
|
39
|
+
).first
|
40
|
+
|
41
|
+
# check whether ID token belongs to currently logged-in user
|
42
|
+
redirect_logout_with_error(oauth_invalid_client_message) unless oauth_grant && claims["sub"] == jwt_subject(oauth_grant,
|
43
|
+
oauth_application)
|
44
|
+
|
45
|
+
# When an id_token_hint parameter is present, the OP MUST validate that it was the issuer of the ID Token.
|
46
|
+
redirect_logout_with_error(oauth_invalid_client_message) unless claims && claims["iss"] == oauth_jwt_issuer
|
47
|
+
|
48
|
+
# now let's logout from IdP
|
49
|
+
transaction do
|
50
|
+
before_logout
|
51
|
+
logout
|
52
|
+
after_logout
|
53
|
+
end
|
54
|
+
|
55
|
+
error_message = logout_notice_flash
|
56
|
+
|
57
|
+
if (post_logout_redirect_uri = param_or_nil("post_logout_redirect_uri"))
|
58
|
+
error_message = catch(:default_logout_redirect) do
|
59
|
+
oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
|
60
|
+
|
61
|
+
throw(:default_logout_redirect, oauth_invalid_client_message) unless oauth_application
|
62
|
+
|
63
|
+
post_logout_redirect_uris = oauth_application[oauth_applications_post_logout_redirect_uris_column].split(" ")
|
64
|
+
|
65
|
+
unless post_logout_redirect_uris.include?(post_logout_redirect_uri)
|
66
|
+
throw(:default_logout_redirect,
|
67
|
+
oauth_invalid_post_logout_redirect_uri_message)
|
68
|
+
end
|
69
|
+
|
70
|
+
if (state = param_or_nil("state"))
|
71
|
+
post_logout_redirect_uri = URI(post_logout_redirect_uri)
|
72
|
+
params = ["state=#{CGI.escape(state)}"]
|
73
|
+
params << post_logout_redirect_uri.query if post_logout_redirect_uri.query
|
74
|
+
post_logout_redirect_uri.query = params.join("&")
|
75
|
+
post_logout_redirect_uri = post_logout_redirect_uri.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
redirect(post_logout_redirect_uri)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
redirect_logout_with_error(error_message)
|
84
|
+
end
|
85
|
+
|
86
|
+
redirect_response_error("invalid_request")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Logout
|
93
|
+
|
94
|
+
def validate_oidc_logout_params
|
95
|
+
redirect_logout_with_error(oauth_invalid_client_message) unless param_or_nil("id_token_hint")
|
96
|
+
# check if valid token hint type
|
97
|
+
return unless (redirect_uri = param_or_nil("post_logout_redirect_uri"))
|
98
|
+
|
99
|
+
return if check_valid_no_fragment_uri?(redirect_uri)
|
100
|
+
|
101
|
+
redirect_logout_with_error(oauth_invalid_client_message)
|
102
|
+
end
|
103
|
+
|
104
|
+
def redirect_logout_with_error(error_message = oauth_invalid_client_message)
|
105
|
+
set_notice_flash(error_message)
|
106
|
+
redirect(logout_redirect)
|
107
|
+
end
|
108
|
+
|
109
|
+
def oauth_server_metadata_body(*)
|
110
|
+
super.tap do |data|
|
111
|
+
data[:end_session_endpoint] = oidc_logout_url
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -30,13 +30,14 @@ module Rodauth
|
|
30
30
|
end
|
31
31
|
|
32
32
|
if dataset.respond_to?(:supports_insert_conflict?) && dataset.supports_insert_conflict?
|
33
|
-
def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil,
|
34
|
-
to_update = params.keys - unique_columns
|
35
|
-
|
33
|
+
def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, to_update_extra = nil)
|
34
|
+
to_update = Hash[(params.keys - unique_columns).map { |attribute| [attribute, Sequel[:excluded][attribute]] }]
|
35
|
+
|
36
|
+
to_update.merge!(to_update_extra) if to_update_extra
|
36
37
|
|
37
38
|
dataset = dataset.insert_conflict(
|
38
39
|
target: unique_columns,
|
39
|
-
update:
|
40
|
+
update: to_update,
|
40
41
|
update_where: conds
|
41
42
|
)
|
42
43
|
|
@@ -51,7 +52,7 @@ module Rodauth
|
|
51
52
|
) || dataset.where(params).first
|
52
53
|
end
|
53
54
|
else
|
54
|
-
def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil,
|
55
|
+
def __insert_or_update_and_return__(dataset, pkey, unique_columns, params, conds = nil, to_update_extra = nil)
|
55
56
|
find_params, update_params = params.partition { |key, _| unique_columns.include?(key) }.map { |h| Hash[h] }
|
56
57
|
|
57
58
|
dataset_where = dataset.where(find_params)
|
@@ -67,7 +68,8 @@ module Rodauth
|
|
67
68
|
end
|
68
69
|
|
69
70
|
if record
|
70
|
-
update_params.
|
71
|
+
update_params.merge!(to_update_extra) if to_update_extra
|
72
|
+
|
71
73
|
__update_and_return__(dataset_where, update_params)
|
72
74
|
else
|
73
75
|
__insert_and_return__(dataset, pkey, params)
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "net/http"
|
5
|
+
require "rodauth/oauth/ttl_store"
|
6
|
+
|
7
|
+
module Rodauth
|
8
|
+
module OAuth
|
9
|
+
module HTTPExtensions
|
10
|
+
REQUEST_CACHE = OAuth::TtlStore.new
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def http_request(uri, form_data = nil)
|
15
|
+
uri = URI(uri)
|
16
|
+
|
17
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
18
|
+
http.use_ssl = uri.scheme == "https"
|
19
|
+
http.open_timeout = 15
|
20
|
+
http.read_timeout = 15
|
21
|
+
http.write_timeout = 15 if http.respond_to?(:write_timeout)
|
22
|
+
|
23
|
+
if form_data
|
24
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
25
|
+
request["content-type"] = "application/x-www-form-urlencoded"
|
26
|
+
request.set_form_data(form_data)
|
27
|
+
else
|
28
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
29
|
+
end
|
30
|
+
request["accept"] = json_response_content_type
|
31
|
+
|
32
|
+
yield request if block_given?
|
33
|
+
|
34
|
+
response = http.request(request)
|
35
|
+
authorization_required unless response.code.to_i == 200
|
36
|
+
response
|
37
|
+
end
|
38
|
+
|
39
|
+
def http_request_with_cache(uri, *args)
|
40
|
+
uri = URI(uri)
|
41
|
+
|
42
|
+
response = http_request_cache[uri]
|
43
|
+
|
44
|
+
return response if response
|
45
|
+
|
46
|
+
http_request_cache.set(uri) do
|
47
|
+
response = http_request(uri, *args)
|
48
|
+
ttl = if response.key?("cache-control")
|
49
|
+
cache_control = response["cache-control"]
|
50
|
+
if cache_control.include?("no-cache")
|
51
|
+
nil
|
52
|
+
else
|
53
|
+
max_age = cache_control[/max-age=(\d+)/, 1].to_i
|
54
|
+
max_age.zero? ? nil : max_age
|
55
|
+
end
|
56
|
+
elsif response.key?("expires")
|
57
|
+
expires = response["expires"]
|
58
|
+
begin
|
59
|
+
Time.parse(expires).to_i - Time.now.to_i
|
60
|
+
rescue ArgumentError
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
[JSON.parse(response.body, symbolize_names: true), ttl]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def http_request_cache
|
70
|
+
REQUEST_CACHE
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -2,7 +2,27 @@
|
|
2
2
|
|
3
3
|
module Rodauth
|
4
4
|
module OAuth
|
5
|
+
module ControllerMethods
|
6
|
+
def self.included(controller)
|
7
|
+
# ActionController::API doesn't have helper methods
|
8
|
+
controller.helper_method :current_oauth_account, :current_oauth_application if controller.respond_to?(:helper_method)
|
9
|
+
end
|
10
|
+
|
11
|
+
def current_oauth_account(name = nil)
|
12
|
+
rodauth(name).current_oauth_account
|
13
|
+
end
|
14
|
+
|
15
|
+
def current_oauth_application(name = nil)
|
16
|
+
rodauth(name).current_oauth_application
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
5
20
|
class Railtie < ::Rails::Railtie
|
21
|
+
initializer "rodauth.controller" do
|
22
|
+
ActiveSupport.on_load(:action_controller) do
|
23
|
+
include ControllerMethods
|
24
|
+
end
|
25
|
+
end
|
6
26
|
end
|
7
27
|
end
|
8
28
|
end
|
@@ -28,6 +28,8 @@ class Rodauth::OAuth::TtlStore
|
|
28
28
|
|
29
29
|
payload, ttl = block.call
|
30
30
|
|
31
|
+
return payload unless ttl
|
32
|
+
|
31
33
|
@store_mutex.synchronize do
|
32
34
|
# given that the block call triggers network, and two requests for the same key be processed
|
33
35
|
# at the same time, this ensures the first one wins.
|
data/lib/rodauth/oauth.rb
CHANGED
@@ -1,7 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "rodauth"
|
4
|
-
|
5
4
|
require "rodauth/oauth/version"
|
6
5
|
|
6
|
+
module Rodauth
|
7
|
+
module OAuth
|
8
|
+
module FeatureExtensions
|
9
|
+
def auth_server_route(*args, &blk)
|
10
|
+
routes = route(*args, &blk)
|
11
|
+
|
12
|
+
handle_meth = routes.last
|
13
|
+
|
14
|
+
define_method(:"#{handle_meth}_for_auth_server") do
|
15
|
+
next unless is_authorization_server?
|
16
|
+
|
17
|
+
send(:"#{handle_meth}_not_for_auth_server")
|
18
|
+
end
|
19
|
+
|
20
|
+
alias_method :"#{handle_meth}_not_for_auth_server", handle_meth
|
21
|
+
alias_method handle_meth, :"#{handle_meth}_for_auth_server"
|
22
|
+
end
|
23
|
+
|
24
|
+
# override
|
25
|
+
def translatable_method(meth, value)
|
26
|
+
define_method(meth) { |*args| translate(meth, value, *args) }
|
27
|
+
auth_value_methods(meth)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
Feature.prepend OAuth::FeatureExtensions
|
33
|
+
end
|
34
|
+
|
7
35
|
require "rodauth/oauth/railtie" if defined?(Rails)
|
data/locales/en.yml
CHANGED
@@ -3,21 +3,29 @@ en:
|
|
3
3
|
require_authorization_error_flash: "Please authorize to continue"
|
4
4
|
create_oauth_application_error_flash: "There was an error registering your oauth application"
|
5
5
|
create_oauth_application_notice_flash: "Your oauth application has been registered"
|
6
|
-
revoke_unauthorized_account_error_flash: "You are not authorized to revoke this
|
7
|
-
|
6
|
+
revoke_unauthorized_account_error_flash: "You are not authorized to revoke this grant"
|
7
|
+
revoke_oauth_grant_notice_flash: "The oauth grant has been revoked"
|
8
8
|
device_verification_notice_flash: "The device is verified"
|
9
9
|
user_code_not_found_error_flash: "No device to authorize with the given user code"
|
10
10
|
authorize_page_title: "Authorize"
|
11
|
+
authorize_page_lead: "The application %{name} would like to access your data."
|
12
|
+
oauth_cancel_button: "Cancel"
|
11
13
|
oauth_applications_page_title: "Oauth Applications"
|
12
14
|
oauth_application_page_title: "Oauth Application"
|
13
15
|
new_oauth_application_page_title: "New Oauth Application"
|
14
|
-
|
15
|
-
|
16
|
+
oauth_application_oauth_grants_page_title: "Application Oauth Grants"
|
17
|
+
oauth_grants_page_title: "My Oauth Grants"
|
16
18
|
device_verification_page_title: "Device Verification"
|
17
19
|
device_search_page_title: "Device Search"
|
18
20
|
oauth_management_pagination_previous_button: "Previous"
|
19
21
|
oauth_management_pagination_next_button: "Next"
|
20
|
-
|
22
|
+
oauth_grants_type_label: "Grant Type"
|
23
|
+
oauth_grants_scopes_label: "Scopes"
|
24
|
+
oauth_grants_token_label: "Token"
|
25
|
+
oauth_grants_refresh_token_label: "Refresh Token"
|
26
|
+
oauth_grants_expires_in_label: "Expires In"
|
27
|
+
oauth_grants_revoked_at_label: "Revoked at"
|
28
|
+
oauth_no_grants_text: "No oauth grants yet!"
|
21
29
|
oauth_applications_name_label: "Name"
|
22
30
|
oauth_applications_description_label: "Description"
|
23
31
|
oauth_applications_scopes_label: "Default scopes"
|
@@ -28,30 +36,34 @@ en:
|
|
28
36
|
oauth_applications_redirect_uri_label: "Redirect URL"
|
29
37
|
oauth_applications_client_secret_label: "Client Secret"
|
30
38
|
oauth_applications_client_id_label: "Client ID"
|
39
|
+
oauth_no_applications_text: "No oauth applications yet!"
|
31
40
|
oauth_grant_user_code_label: "User code"
|
32
41
|
oauth_grant_user_jws_jwk_label: "JSON Web Keys"
|
33
42
|
oauth_grant_user_jwt_public_key_label: "Public key"
|
34
43
|
oauth_application_button: "Register"
|
35
44
|
oauth_authorize_button: "Authorize"
|
36
|
-
|
45
|
+
oauth_grant_revoke_button: "Revoke"
|
37
46
|
oauth_authorize_post_button: "Back to Client Application"
|
47
|
+
oauth_device_verification_page_lead: "The device with user code %{user_code} would like to access your data."
|
38
48
|
oauth_device_verification_button: "Verify"
|
49
|
+
oauth_device_search_page_lead: "Insert the user code from the device you'd like to authorize."
|
39
50
|
oauth_device_search_button: "Search"
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
51
|
+
oauth_invalid_client_message: "Client authentication failed"
|
52
|
+
oauth_invalid_grant_type_message: "Invalid grant type"
|
53
|
+
oauth_invalid_grant_message: "Invalid grant"
|
54
|
+
oauth_invalid_scope_message: "Invalid scope"
|
44
55
|
invalid_url_message: "Invalid URL"
|
45
|
-
|
46
|
-
unique_error_message: "is already in use"
|
56
|
+
oauth_unsupported_token_type_message: "Invalid token type hint"
|
47
57
|
null_error_message: "is not filled"
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
+
oauth_unsupported_response_type_message: "Unsupported response type"
|
59
|
+
oauth_already_in_use_message: "error generating unique token"
|
60
|
+
oauth_expired_token_message: "the device code has expired"
|
61
|
+
oauth_access_denied_message: "the authorization request has been denied"
|
62
|
+
oauth_authorization_pending_message: "the authorization request is still pending"
|
63
|
+
oauth_slow_down_message: "authorization request is still pending but poll interval should be increased"
|
64
|
+
oauth_code_challenge_required_message: "code challenge required"
|
65
|
+
oauth_unsupported_transform_algorithm_message: "transform algorithm not supported"
|
66
|
+
oauth_invalid_request_object_message: "request object is invalid"
|
67
|
+
oauth_invalid_scope_message: "The Access Token expired"
|
68
|
+
oauth_authorize_parameter_required: "'%{parameter}' is a required parameter"
|
69
|
+
oauth_invalid_post_logout_redirect_uri_message: "Invalid post logout redirect URI"
|