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

Sign up to get free protection for your applications and to get access to all the features.
@@ -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