rodauth-oauth 0.10.4 → 1.0.0.pre.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/MIGRATION-GUIDE-v1.md +286 -0
  3. data/README.md +28 -35
  4. data/doc/release_notes/1_0_0_beta1.md +38 -0
  5. data/doc/release_notes/1_0_0_beta2.md +34 -0
  6. data/lib/generators/rodauth/oauth/install_generator.rb +0 -1
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +21 -11
  8. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb +1 -1
  9. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb +2 -2
  10. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb +1 -6
  11. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb +0 -2
  12. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb +41 -0
  13. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +2 -2
  14. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_grants.html.erb +37 -0
  15. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +57 -57
  16. data/lib/rodauth/features/oauth_application_management.rb +61 -74
  17. data/lib/rodauth/features/oauth_assertion_base.rb +19 -23
  18. data/lib/rodauth/features/oauth_authorization_code_grant.rb +62 -90
  19. data/lib/rodauth/features/oauth_authorize_base.rb +115 -22
  20. data/lib/rodauth/features/oauth_base.rb +397 -315
  21. data/lib/rodauth/features/oauth_client_credentials_grant.rb +20 -18
  22. data/lib/rodauth/features/{oauth_device_grant.rb → oauth_device_code_grant.rb} +62 -73
  23. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +52 -31
  24. data/lib/rodauth/features/oauth_grant_management.rb +70 -0
  25. data/lib/rodauth/features/oauth_implicit_grant.rb +29 -27
  26. data/lib/rodauth/features/oauth_jwt.rb +53 -689
  27. data/lib/rodauth/features/oauth_jwt_base.rb +458 -0
  28. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +48 -17
  29. data/lib/rodauth/features/oauth_jwt_jwks.rb +47 -0
  30. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +116 -0
  31. data/lib/rodauth/features/oauth_management_base.rb +2 -0
  32. data/lib/rodauth/features/oauth_pkce.rb +22 -26
  33. data/lib/rodauth/features/oauth_resource_indicators.rb +33 -25
  34. data/lib/rodauth/features/oauth_resource_server.rb +59 -0
  35. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +7 -1
  36. data/lib/rodauth/features/oauth_token_introspection.rb +76 -46
  37. data/lib/rodauth/features/oauth_token_revocation.rb +46 -33
  38. data/lib/rodauth/features/oidc.rb +382 -241
  39. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +127 -51
  40. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +115 -0
  41. data/lib/rodauth/oauth/database_extensions.rb +8 -6
  42. data/lib/rodauth/oauth/http_extensions.rb +74 -0
  43. data/lib/rodauth/oauth/railtie.rb +20 -0
  44. data/lib/rodauth/oauth/ttl_store.rb +2 -0
  45. data/lib/rodauth/oauth/version.rb +1 -1
  46. data/lib/rodauth/oauth.rb +29 -1
  47. data/locales/en.yml +34 -22
  48. data/locales/pt.yml +34 -22
  49. data/templates/authorize.str +19 -17
  50. data/templates/device_search.str +1 -1
  51. data/templates/device_verification.str +2 -2
  52. data/templates/jwks_field.str +1 -0
  53. data/templates/new_oauth_application.str +1 -2
  54. data/templates/oauth_application.str +2 -2
  55. data/templates/oauth_application_oauth_grants.str +54 -0
  56. data/templates/oauth_applications.str +2 -2
  57. data/templates/oauth_grants.str +52 -0
  58. metadata +23 -16
  59. data/lib/generators/rodauth/oauth/templates/app/models/oauth_token.rb +0 -4
  60. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_tokens.html.erb +0 -39
  61. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_tokens.html.erb +0 -35
  62. data/lib/rodauth/features/oauth.rb +0 -9
  63. data/lib/rodauth/features/oauth_http_mac.rb +0 -86
  64. data/lib/rodauth/features/oauth_token_management.rb +0 -81
  65. data/lib/rodauth/oauth/refinements.rb +0 -48
  66. data/templates/jwt_public_key_field.str +0 -4
  67. data/templates/oauth_application_oauth_tokens.str +0 -52
  68. 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
- elsif (value = @oauth_application_params[oauth_applications_subject_type_column])
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", register_invalid_param_message("subject_type"))
72
+ register_throw_json_response_error("invalid_client_metadata", register_invalid_client_metadata_message("subject_type", value))
49
73
  end
50
- elsif (value = @oauth_application_params[oauth_applications_id_token_signed_response_alg_column])
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 !oauth_jwt_algorithms_supported.include?(value)
59
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_signed_response_alg"))
60
- end
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
- elsif (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_enc_column])
78
- unless oauth_jwt_jwe_encryption_methods_supported.include?(value)
79
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("userinfo_encrypted_response_enc"))
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
- elsif (value = @oauth_application_params[oauth_applications_request_object_signing_alg_column])
82
- unless oauth_jwt_algorithms_supported.include?(value)
83
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("request_object_signing_alg"))
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
- elsif (value = @oauth_application_params[oauth_applications_request_object_encryption_alg_column])
86
- unless oauth_jwt_jwe_algorithms_supported.include?(value)
87
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("request_object_encryption_alg"))
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
- elsif (value = @oauth_application_params[oauth_applications_request_object_encryption_enc_column])
90
- unless oauth_jwt_jwe_encryption_methods_supported.include?(value)
91
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("request_object_encryption_enc"))
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] ||= begin
118
- return_params["id_token_signed_response_alg"] = oauth_jwt_algorithm
119
- oauth_jwt_algorithm
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] ||= begin
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
- end
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] ||= begin
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
- end
207
+
132
208
  end
133
- if create_params.key?(oauth_applications_request_object_encryption_alg_column)
134
- create_params[oauth_applications_request_object_encryption_enc_column] ||= begin
135
- return_params["request_object_encryption_enc"] = "A128CBC-HS256"
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
- end
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, exclude_on_update = nil)
34
- to_update = params.keys - unique_columns
35
- to_update -= exclude_on_update if exclude_on_update
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: Hash[ to_update.map { |attribute| [attribute, Sequel[:excluded][attribute]] } ],
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, exclude_on_update = 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.reject! { |k, _v| exclude_on_update.include?(k) } if exclude_on_update
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.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "0.10.4"
5
+ VERSION = "1.0.0-beta2"
6
6
  end
7
7
  end
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 token"
7
- revoke_oauth_token_notice_flash: "The oauth token has been revoked"
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
- oauth_application_oauth_tokens_page_title: "Application Oauth Tokens"
15
- oauth_tokens_page_title: "My Oauth Tokens"
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
- oauth_tokens_scopes_label: "Scopes"
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
- oauth_token_revoke_button: "Revoke"
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
- invalid_client_message: "Client authentication failed"
41
- invalid_grant_type_message: "Invalid grant type"
42
- invalid_grant_message: "Invalid grant"
43
- invalid_scope_message: "Invalid scope"
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
- unsupported_token_type_message: "Invalid token type hint"
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
- already_in_use_message: "error generating unique token"
49
- expired_token_message: "the device code has expired"
50
- access_denied_message: "the authorization request has been denied"
51
- authorization_pending_message: "the authorization request is still pending"
52
- slow_down_message: "authorization request is still pending but poll interval should be increased"
53
- code_challenge_required_message: "code challenge required"
54
- unsupported_transform_algorithm_message: "transform algorithm not supported"
55
- request_uri_not_supported_message: "request uri is unsupported"
56
- invalid_request_object_message: "request object is invalid"
57
- invalid_scope_message: "The Access Token expired"
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"