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.
@@ -51,9 +51,25 @@ module Rodauth
51
51
  end
52
52
  end
53
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
+
54
70
  if (value = @oauth_application_params[oauth_applications_subject_type_column])
55
71
  unless %w[pairwise public].include?(value)
56
- 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))
57
73
  end
58
74
 
59
75
  if value == "pairwise"
@@ -84,51 +100,75 @@ module Rodauth
84
100
  register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_signed_response_alg"))
85
101
  end
86
102
  elsif !oauth_jwt_jws_algorithms_supported.include?(value)
87
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_signed_response_alg"))
103
+ register_throw_json_response_error("invalid_client_metadata",
104
+ register_invalid_client_metadata_message("id_token_signed_response_alg", value))
88
105
  end
89
106
  end
90
107
 
91
- if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_alg_column]) &&
92
- !oauth_jwt_jwe_algorithms_supported.include?(value)
93
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_encrypted_response_alg"))
94
- end
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))
114
+ end
95
115
 
96
- if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_enc_column]) &&
97
- !oauth_jwt_jwe_encryption_methods_supported.include?(value)
98
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("id_token_encrypted_response_enc"))
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))
121
+ end
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))
128
+ end
99
129
  end
100
130
 
101
- if (value = @oauth_application_params[oauth_applications_userinfo_signed_response_alg_column]) &&
102
- !oauth_jwt_jws_algorithms_supported.include?(value)
103
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("userinfo_signed_response_alg"))
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))
141
+ end
104
142
  end
105
143
 
106
- if (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_alg_column]) &&
144
+ if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_alg_column]) &&
107
145
  !oauth_jwt_jwe_algorithms_supported.include?(value)
108
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("userinfo_encrypted_response_alg"))
146
+ register_throw_json_response_error("invalid_client_metadata",
147
+ register_invalid_client_metadata_message("id_token_encrypted_response_alg", value))
109
148
  end
110
149
 
111
- if (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_enc_column]) &&
150
+ if (value = @oauth_application_params[oauth_applications_id_token_encrypted_response_enc_column]) &&
112
151
  !oauth_jwt_jwe_encryption_methods_supported.include?(value)
113
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("userinfo_encrypted_response_enc"))
152
+ register_throw_json_response_error("invalid_client_metadata",
153
+ register_invalid_client_metadata_message("id_token_encrypted_response_enc", value))
114
154
  end
115
155
 
116
- if defined?(oauth_applications_request_object_signing_alg_column) &&
117
- (value = @oauth_application_params[oauth_applications_request_object_signing_alg_column]) &&
156
+ if (value = @oauth_application_params[oauth_applications_userinfo_signed_response_alg_column]) &&
118
157
  !oauth_jwt_jws_algorithms_supported.include?(value)
119
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("request_object_signing_alg"))
158
+ register_throw_json_response_error("invalid_client_metadata",
159
+ register_invalid_client_metadata_message("userinfo_signed_response_alg", value))
120
160
  end
121
161
 
122
- if defined?(oauth_applications_request_object_encryption_alg_column) &&
123
- (value = @oauth_application_params[oauth_applications_request_object_encryption_alg_column]) &&
162
+ if (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_alg_column]) &&
124
163
  !oauth_jwt_jwe_algorithms_supported.include?(value)
125
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("request_object_encryption_alg"))
164
+ register_throw_json_response_error("invalid_client_metadata",
165
+ register_invalid_client_metadata_message("userinfo_encrypted_response_alg", value))
126
166
  end
127
167
 
128
- if defined?(oauth_applications_request_object_encryption_enc_column) &&
129
- (value = @oauth_application_params[oauth_applications_request_object_encryption_enc_column]) &&
168
+ if (value = @oauth_application_params[oauth_applications_userinfo_encrypted_response_enc_column]) &&
130
169
  !oauth_jwt_jwe_encryption_methods_supported.include?(value)
131
- register_throw_json_response_error("invalid_client_metadata", register_invalid_param_message("request_object_encryption_enc"))
170
+ register_throw_json_response_error("invalid_client_metadata",
171
+ register_invalid_client_metadata_message("userinfo_encrypted_response_enc", value))
132
172
  end
133
173
  end
134
174
 
@@ -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
@@ -16,6 +16,9 @@ module Rodauth
16
16
 
17
17
  http = Net::HTTP.new(uri.host, uri.port)
18
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)
19
22
 
20
23
  if form_data
21
24
  request = Net::HTTP::Post.new(uri.request_uri)
@@ -44,9 +47,19 @@ module Rodauth
44
47
  response = http_request(uri, *args)
45
48
  ttl = if response.key?("cache-control")
46
49
  cache_control = response["cache-control"]
47
- cache_control[/max-age=(\d+)/, 1].to_i
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
48
56
  elsif response.key?("expires")
49
- Time.parse(response["expires"]).to_i - Time.now.to_i
57
+ expires = response["expires"]
58
+ begin
59
+ Time.parse(expires).to_i - Time.now.to_i
60
+ rescue ArgumentError
61
+ nil
62
+ end
50
63
  end
51
64
 
52
65
  [JSON.parse(response.body, symbolize_names: true), ttl]
@@ -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 = "1.0.0-beta1"
5
+ VERSION = "1.0.0-beta2"
6
6
  end
7
7
  end
data/locales/en.yml CHANGED
@@ -55,6 +55,7 @@ en:
55
55
  invalid_url_message: "Invalid URL"
56
56
  oauth_unsupported_token_type_message: "Invalid token type hint"
57
57
  null_error_message: "is not filled"
58
+ oauth_unsupported_response_type_message: "Unsupported response type"
58
59
  oauth_already_in_use_message: "error generating unique token"
59
60
  oauth_expired_token_message: "the device code has expired"
60
61
  oauth_access_denied_message: "the authorization request has been denied"
@@ -62,6 +63,7 @@ en:
62
63
  oauth_slow_down_message: "authorization request is still pending but poll interval should be increased"
63
64
  oauth_code_challenge_required_message: "code challenge required"
64
65
  oauth_unsupported_transform_algorithm_message: "transform algorithm not supported"
65
- oauth_request_uri_not_supported_message: "request uri is unsupported"
66
66
  oauth_invalid_request_object_message: "request object is invalid"
67
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"
data/locales/pt.yml CHANGED
@@ -55,6 +55,7 @@ pt:
55
55
  invalid_url_message: "URL inválido"
56
56
  oauth_unsupported_token_type_message: "Sugestão de tipo de token inválida"
57
57
  null_error_message: "não está preenchido"
58
+ oauth_unsupported_response_type_message: "Tipo de resposta inválido"
58
59
  oauth_already_in_use_message: "erro ao gerar token único"
59
60
  oauth_expired_token_message: "o código de dispositivo expirou"
60
61
  oauth_access_denied_message: "o pedido de autorização foi negado"
@@ -62,6 +63,7 @@ pt:
62
63
  oauth_slow_down_message: "o pedido de autorização ainda está pendente mas o intervalo de actualização deve ser aumentado"
63
64
  oauth_code_challenge_required_message: "código de negociação necessário"
64
65
  oauth_unsupported_transform_algorithm_message: "algoritmo de transformação não suportado"
65
- oauth_request_uri_not_supported_message: "request_uri não é suportado"
66
66
  oauth_invalid_request_object_message: "request_object é inválido"
67
67
  oauth_invalid_scope_message: "O Token de acesso expirou"
68
+ oauth_authorize_parameter_required: "'%{parameter}' é um parâmetro obrigatório"
69
+ oauth_invalid_post_logout_redirect_uri_message: "URI de redireccionamento pós-logout inválido"
@@ -9,12 +9,13 @@
9
9
  }
10
10
  <p class="lead">
11
11
  #{
12
- rodauth.authorize_page_lead(name: <<-LINK
13
- <a target="_blank" href="#{h(rodauth.oauth_application[rodauth.oauth_applications_homepage_url_column])}">
12
+ application_uri = rodauth.oauth_application[rodauth.oauth_applications_homepage_url_column]
13
+ application_name = application_uri ? (<<-LINK) : rodauth.oauth_application[rodauth.oauth_applications_name_column]
14
+ <a target="_blank" href="#{h(application_uri)}">
14
15
  #{h(rodauth.oauth_application[rodauth.oauth_applications_name_column])}
15
16
  </a>
16
17
  LINK
17
- )
18
+ rodauth.authorize_page_lead(name: application_name)
18
19
  }
19
20
  </p>
20
21
  <div class="list-group">
@@ -60,12 +61,16 @@
60
61
 
61
62
  #{
62
63
  rodauth.authorize_scopes.map do |scope|
63
- <<-HTML
64
- <div class="form-check">
65
- <input id="#{scope}" class="form-check-input" type="checkbox" name="scope[]" value="#{h(scope)}">
66
- <label class="form-check-label" for="#{scope}">#{h(scope)}</label>
67
- </div>
68
- HTML
64
+ if rodauth.features.include?(:oidc) && scope == "offline_access"
65
+ "<input type=\"hidden\" name=\"scope[]\" value=\"#{scope}\" />"
66
+ else
67
+ <<-HTML
68
+ <div class="form-check">
69
+ <input id="#{scope}" class="form-check-input" type="checkbox" name="scope[]" value="#{h(scope)}">
70
+ <label class="form-check-label" for="#{scope}">#{h(scope)}</label>
71
+ </div>
72
+ HTML
73
+ end
69
74
  end.join
70
75
  }
71
76
 
@@ -77,10 +82,12 @@
77
82
  #{"<input type=\"hidden\" name=\"redirect_uri\" value=\"#{rodauth.redirect_uri}\"/>" if rodauth.param_or_nil("redirect_uri")}
78
83
  #{"<input type=\"hidden\" name=\"code_challenge\" value=\"#{rodauth.param("code_challenge")}\"/>" if rodauth.features.include?(:oauth_pkce) && rodauth.param_or_nil("code_challenge")}
79
84
  #{"<input type=\"hidden\" name=\"code_challenge_method\" value=\"#{rodauth.param("code_challenge_method")}\"/>" if rodauth.features.include?(:oauth_pkce) && rodauth.param_or_nil("code_challenge_method")}
85
+ #{"<input type=\"hidden\" name=\"prompt\" value=\"#{rodauth.param("prompt")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("prompt")}
80
86
  #{"<input type=\"hidden\" name=\"nonce\" value=\"#{rodauth.param("nonce")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("nonce")}
81
87
  #{"<input type=\"hidden\" name=\"ui_locales\" value=\"#{rodauth.param("ui_locales")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("ui_locales")}
82
88
  #{"<input type=\"hidden\" name=\"claims_locales\" value=\"#{rodauth.param("claims_locales")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("claims_locales")}
83
- #{"<input type=\"hidden\" name=\"acr\" value=\"#{rodauth.param("acr_values")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("acr_values")}
89
+ #{"<input type=\"hidden\" name=\"claims\" value=\"#{h(rodauth.param("claims"))}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("claims")}
90
+ #{"<input type=\"hidden\" name=\"acr_values\" value=\"#{rodauth.param("acr_values")}\"/>" if rodauth.features.include?(:oidc) && rodauth.param_or_nil("acr_values")}
84
91
  #{
85
92
  if rodauth.features.include?(:oauth_resource_indicators) && rodauth.resource_indicators
86
93
  rodauth.resource_indicators.map do |resource|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rodauth-oauth
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.beta1
4
+ version: 1.0.0.pre.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-21 00:00:00.000000000 Z
11
+ date: 2022-11-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rodauth
@@ -67,6 +67,7 @@ extra_rdoc_files:
67
67
  - doc/release_notes/0_9_2.md
68
68
  - doc/release_notes/0_9_3.md
69
69
  - doc/release_notes/1_0_0_beta1.md
70
+ - doc/release_notes/1_0_0_beta2.md
70
71
  files:
71
72
  - CHANGELOG.md
72
73
  - LICENSE.txt
@@ -105,6 +106,7 @@ files:
105
106
  - doc/release_notes/0_9_2.md
106
107
  - doc/release_notes/0_9_3.md
107
108
  - doc/release_notes/1_0_0_beta1.md
109
+ - doc/release_notes/1_0_0_beta2.md
108
110
  - lib/generators/rodauth/oauth/install_generator.rb
109
111
  - lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
110
112
  - lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
@@ -142,6 +144,7 @@ files:
142
144
  - lib/rodauth/features/oauth_token_revocation.rb
143
145
  - lib/rodauth/features/oidc.rb
144
146
  - lib/rodauth/features/oidc_dynamic_client_registration.rb
147
+ - lib/rodauth/features/oidc_rp_initiated_logout.rb
145
148
  - lib/rodauth/oauth.rb
146
149
  - lib/rodauth/oauth/database_extensions.rb
147
150
  - lib/rodauth/oauth/http_extensions.rb