rodauth-oauth 1.3.2 → 1.4.0

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -10
  3. data/doc/release_notes/1_4_0.md +49 -0
  4. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +23 -23
  5. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/frontchannel_logout.html.erb +10 -0
  6. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +1 -1
  7. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +20 -0
  8. data/lib/generators/rodauth/oauth/views_generator.rb +2 -2
  9. data/lib/rodauth/features/oauth_application_management.rb +1 -1
  10. data/lib/rodauth/features/oauth_assertion_base.rb +1 -1
  11. data/lib/rodauth/features/oauth_authorize_base.rb +1 -1
  12. data/lib/rodauth/features/oauth_base.rb +31 -30
  13. data/lib/rodauth/features/oauth_device_code_grant.rb +2 -2
  14. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +1 -0
  15. data/lib/rodauth/features/oauth_grant_management.rb +1 -1
  16. data/lib/rodauth/features/oauth_jwt.rb +3 -3
  17. data/lib/rodauth/features/oauth_jwt_base.rb +15 -10
  18. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +1 -1
  19. data/lib/rodauth/features/oauth_jwt_jwks.rb +1 -1
  20. data/lib/rodauth/features/oauth_resource_server.rb +1 -1
  21. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +79 -47
  22. data/lib/rodauth/features/oauth_tls_client_auth.rb +1 -1
  23. data/lib/rodauth/features/oauth_token_introspection.rb +1 -1
  24. data/lib/rodauth/features/oauth_token_revocation.rb +1 -1
  25. data/lib/rodauth/features/oidc.rb +27 -8
  26. data/lib/rodauth/features/oidc_backchannel_logout.rb +120 -0
  27. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +25 -0
  28. data/lib/rodauth/features/oidc_frontchannel_logout.rb +134 -0
  29. data/lib/rodauth/features/oidc_logout_base.rb +76 -0
  30. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +29 -6
  31. data/lib/rodauth/features/oidc_session_management.rb +89 -0
  32. data/lib/rodauth/oauth/http_extensions.rb +1 -1
  33. data/lib/rodauth/oauth/version.rb +1 -1
  34. data/locales/en.yml +9 -0
  35. data/locales/pt.yml +9 -0
  36. data/templates/check_session.str +67 -0
  37. data/templates/frontchannel_logout.str +17 -0
  38. metadata +11 -2
@@ -4,11 +4,15 @@ require "rodauth/oauth"
4
4
 
5
5
  module Rodauth
6
6
  Feature.define(:oidc_rp_initiated_logout, :OidcRpInitiatedLogout) do
7
- depends :oidc
7
+ depends :oidc_logout_base
8
+ response "oidc_logout"
8
9
 
9
10
  auth_value_method :oauth_applications_post_logout_redirect_uris_column, :post_logout_redirect_uris
11
+ translatable_method :oauth_invalid_id_token_hint_message, "Invalid ID token hint"
10
12
  translatable_method :oauth_invalid_post_logout_redirect_uri_message, "Invalid post logout redirect URI"
11
13
 
14
+ attr_reader :oidc_logout_redirect
15
+
12
16
  # /oidc-logout
13
17
  auth_server_route(:oidc_logout) do |r|
14
18
  require_authorizable_account
@@ -19,7 +23,7 @@ module Rodauth
19
23
  catch_error do
20
24
  validate_oidc_logout_params
21
25
 
22
- oauth_application = nil
26
+ claims = oauth_application = nil
23
27
 
24
28
  if (id_token_hint = param_or_nil("id_token_hint"))
25
29
  #
@@ -32,7 +36,12 @@ module Rodauth
32
36
  #
33
37
  claims = jwt_decode(id_token_hint, verify_claims: false)
34
38
 
35
- redirect_logout_with_error(oauth_invalid_client_message) unless claims
39
+ redirect_logout_with_error(oauth_invalid_id_token_hint_message) unless claims
40
+
41
+ # If the ID Token's sid claim does not correspond to the RP's current session or a
42
+ # recent session at the OP, the OP SHOULD treat the logout request as suspect, and
43
+ # MAY decline to act upon it.
44
+ redirect_logout_with_error(oauth_invalid_client_message) if claims["sid"] && !active_sessions?(claims["sid"])
36
45
 
37
46
  oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["aud"]).first
38
47
  oauth_grant = db[oauth_grants_table]
@@ -40,9 +49,15 @@ module Rodauth
40
49
  .where(oauth_grants_oauth_application_id_column => oauth_application[oauth_applications_id_column])
41
50
  .first
42
51
 
52
+ unique_account_id = if oauth_grant
53
+ oauth_grant[oauth_grants_account_id_column]
54
+ else
55
+ account_id
56
+ end
57
+
43
58
  # check whether ID token belongs to currently logged-in user
44
- redirect_logout_with_error(oauth_invalid_client_message) unless oauth_grant && claims["sub"] == jwt_subject(oauth_grant,
45
- oauth_application)
59
+ redirect_logout_with_error(oauth_invalid_client_message) unless claims["sub"] == jwt_subject(unique_account_id,
60
+ oauth_application)
46
61
 
47
62
  # When an id_token_hint parameter is present, the OP MUST validate that it was the issuer of the ID Token.
48
63
  redirect_logout_with_error(oauth_invalid_client_message) unless claims && claims["iss"] == oauth_jwt_issuer
@@ -59,6 +74,8 @@ module Rodauth
59
74
 
60
75
  if (post_logout_redirect_uri = param_or_nil("post_logout_redirect_uri"))
61
76
  error_message = catch(:default_logout_redirect) do
77
+ throw(:default_logout_redirect, oauth_invalid_id_token_hint_message) unless claims
78
+
62
79
  oauth_application = db[oauth_applications_table].where(oauth_applications_client_id_column => claims["client_id"]).first
63
80
 
64
81
  throw(:default_logout_redirect, oauth_invalid_client_message) unless oauth_application
@@ -78,7 +95,9 @@ module Rodauth
78
95
  post_logout_redirect_uri = post_logout_redirect_uri.to_s
79
96
  end
80
97
 
81
- redirect(post_logout_redirect_uri)
98
+ @oidc_logout_redirect = post_logout_redirect_uri
99
+
100
+ require_response(:_oidc_logout_response)
82
101
  end
83
102
 
84
103
  end
@@ -90,6 +109,10 @@ module Rodauth
90
109
  end
91
110
  end
92
111
 
112
+ def _oidc_logout_response
113
+ redirect(oidc_logout_redirect)
114
+ end
115
+
93
116
  private
94
117
 
95
118
  # Logout
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth"
4
+
5
+ module Rodauth
6
+ Feature.define(:oidc_session_management, :OidcSessionManagement) do
7
+ depends :oidc
8
+
9
+ view "check_session", "Check Session", "check_session"
10
+
11
+ auth_value_method :oauth_oidc_user_agent_state_cookie_key, "_rodauth_oauth_user_agent_state"
12
+ auth_value_method :oauth_oidc_user_agent_state_cookie_options, {}.freeze
13
+ auth_value_method :oauth_oidc_user_agent_state_cookie_expires_in, 365 * 24 * 60 * 60 # 1 year
14
+
15
+ auth_value_method :oauth_oidc_user_agent_state_js, nil
16
+
17
+ auth_value_methods(
18
+ :oauth_oidc_session_management_salt
19
+ )
20
+ # /authorize
21
+ auth_server_route(:check_session) do |r|
22
+ allow_cors(r)
23
+
24
+ r.get do
25
+ set_title(:check_session_page_title)
26
+ scope.view(_view_opts("check_session").merge(layout: false))
27
+ end
28
+ end
29
+
30
+ def clear_session
31
+ super
32
+
33
+ # update user agent state in the process
34
+ # TODO: dangerous if this gets overidden by the user
35
+
36
+ user_agent_state_cookie_opts = Hash[oauth_oidc_user_agent_state_cookie_options]
37
+ user_agent_state_cookie_opts[:value] = oauth_unique_id_generator
38
+ user_agent_state_cookie_opts[:expires] = convert_timestamp(Time.now + oauth_oidc_user_agent_state_cookie_expires_in)
39
+ user_agent_state_cookie_opts[:secure] = true
40
+ ::Rack::Utils.set_cookie_header!(response.headers, oauth_oidc_user_agent_state_cookie_key, user_agent_state_cookie_opts)
41
+ end
42
+
43
+ private
44
+
45
+ def do_authorize(*)
46
+ params, mode = super
47
+
48
+ params["session_state"] = generate_session_state
49
+
50
+ [params, mode]
51
+ end
52
+
53
+ def response_error_params(*)
54
+ payload = super
55
+
56
+ return payload unless request.path == authorize_path
57
+
58
+ payload["session_state"] = generate_session_state
59
+ payload
60
+ end
61
+
62
+ def generate_session_state
63
+ salt = oauth_oidc_session_management_salt
64
+
65
+ uri = URI(redirect_uri)
66
+ origin = if uri.respond_to?(:origin)
67
+ uri.origin
68
+ else
69
+ # TODO: remove when not supporting uri < 0.11
70
+ "#{uri.scheme}://#{uri.host}#{":#{uri.port}" if uri.port != uri.default_port}"
71
+ end
72
+ session_id = "#{oauth_application[oauth_applications_client_id_column]} " \
73
+ "#{origin} " \
74
+ "#{request.cookies[oauth_oidc_user_agent_state_cookie_key]} #{salt}"
75
+
76
+ "#{Digest::SHA256.hexdigest(session_id)}.#{salt}"
77
+ end
78
+
79
+ def oauth_server_metadata_body(*)
80
+ super.tap do |data|
81
+ data[:check_session_iframe] = check_session_url
82
+ end
83
+ end
84
+
85
+ def oauth_oidc_session_management_salt
86
+ oauth_unique_id_generator
87
+ end
88
+ end
89
+ end
@@ -32,7 +32,7 @@ module Rodauth
32
32
  yield request if block_given?
33
33
 
34
34
  response = http.request(request)
35
- authorization_required unless response.code.to_i == 200
35
+ authorization_required unless (200..299).include?(response.code.to_i)
36
36
  response
37
37
  end
38
38
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rodauth
4
4
  module OAuth
5
- VERSION = "1.3.2"
5
+ VERSION = "1.4.0"
6
6
  end
7
7
  end
data/locales/en.yml CHANGED
@@ -8,7 +8,9 @@ en:
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
+ frontchannel_logout_page_title: "Logout"
11
12
  authorize_page_lead: "The application %{name} would like to access your data."
13
+ oauth_frontchannel_logout_redirecting_lead: "You are being redirected..."
12
14
  authorize_error_page_title: "Authorize Error"
13
15
  oauth_cancel_button: "Cancel"
14
16
  oauth_applications_page_title: "Oauth Applications"
@@ -18,6 +20,7 @@ en:
18
20
  oauth_grants_page_title: "My Oauth Grants"
19
21
  device_verification_page_title: "Device Verification"
20
22
  device_search_page_title: "Device Search"
23
+ check_session_page_title: "Check Session"
21
24
  oauth_management_pagination_previous_button: "Previous"
22
25
  oauth_management_pagination_next_button: "Next"
23
26
  oauth_grants_type_label: "Grant Type"
@@ -41,6 +44,8 @@ en:
41
44
  oauth_grant_user_code_label: "User code"
42
45
  oauth_grant_user_jws_jwk_label: "JSON Web Keys"
43
46
  oauth_grant_user_jwt_public_key_label: "Public key"
47
+ oauth_frontchannel_logout_redirecting_label: "please click %{link} if your browser does not redirect you in a few seconds."
48
+ oauth_frontchannel_logout_redirecting_link_label: "here"
44
49
  oauth_application_button: "Register"
45
50
  oauth_authorize_button: "Authorize"
46
51
  oauth_grant_revoke_button: "Revoke"
@@ -68,3 +73,7 @@ en:
68
73
  oauth_invalid_scope_message: "The Access Token expired"
69
74
  oauth_authorize_parameter_required: "Invalid or missing '%{parameter}'"
70
75
  oauth_invalid_post_logout_redirect_uri_message: "Invalid post logout redirect URI"
76
+ oauth_saml_assertion_not_base64_message: "SAML assertion must be in base64 format"
77
+ oauth_saml_assertion_single_issuer_message: "SAML assertion must have a single issuer"
78
+ oauth_saml_settings_not_found_message: "No SAML settings found for issuer"
79
+ oauth_invalid_id_token_hint_message: "Invalid ID token hint"
data/locales/pt.yml CHANGED
@@ -8,7 +8,9 @@ pt:
8
8
  device_verification_notice_flash: "O dispositivo foi verificado com sucesso"
9
9
  user_code_not_found_error_flash: "Não existe nenhum dispositivo a ser autorizado com o código de usuário inserido"
10
10
  authorize_page_title: "Autorizar"
11
+ frontchannel_logout_page_title: "Saindo"
11
12
  authorize_page_lead: "O aplicativo %{name} gostaria de aceder aos seus dados."
13
+ oauth_frontchannel_logout_redirecting_lead: "Redireccionando.."
12
14
  authorize_error_page_title: Erro de autorização
13
15
  oauth_cancel_button: "Cancelar"
14
16
  oauth_applications_page_title: "Aplicativos OAuth"
@@ -18,6 +20,7 @@ pt:
18
20
  oauth_grants_page_title: "As minhas concessões Oauth"
19
21
  device_verification_page_title: "Verificação de dispositivo"
20
22
  device_search_page_title: "Pesquisa de dispositivo"
23
+ check_session_page_title: "Verificação de Sessão"
21
24
  oauth_management_pagination_previous_button: "Anterior"
22
25
  oauth_management_pagination_next_button: "Próxima"
23
26
  oauth_grants_type_label: "Tipo de concessão"
@@ -41,6 +44,8 @@ pt:
41
44
  oauth_grant_user_code_label: "Código do usuário"
42
45
  oauth_grant_user_jws_jwk_label: "Chaves JSON Web"
43
46
  oauth_grant_user_jwt_public_key_label: "Chave pública"
47
+ oauth_frontchannel_logout_redirecting_label: "por favor clique %{link} se o seu navegador não lhe redireccionar em alguns segundos."
48
+ oauth_frontchannel_logout_redirecting_link_label: "aqui"
44
49
  oauth_application_button: "Registar"
45
50
  oauth_authorize_button: "Autorizar"
46
51
  oauth_grant_revoke_button: "Revogar"
@@ -68,3 +73,7 @@ pt:
68
73
  oauth_invalid_scope_message: "O Token de acesso expirou"
69
74
  oauth_authorize_parameter_required: "'%{parameter}' inválido ou em falta"
70
75
  oauth_invalid_post_logout_redirect_uri_message: "URI de redireccionamento pós-logout inválido"
76
+ oauth_saml_assertion_not_base64_message: "A asserção SAML tem que estar no formato base64"
77
+ oauth_saml_assertion_single_issuer_message: "A asserção SAML tem que ter um único emissor"
78
+ oauth_saml_settings_not_found_message: "Nenhuma configuração SAML encontrada para o emissor"
79
+ oauth_invalid_id_token_hint_message: "ID token hint inválido"
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>#{@page_title}</title>
7
+ </head>
8
+ <body>
9
+ <script type="text/javascript">
10
+ window.addEventListener("message", receiveMessage, false);
11
+
12
+ function receiveMessage(e) { // e.data has client_id and session_state
13
+ var client_id = e.data.substr(0, e.data.lastIndexOf(' '));
14
+ var session_state = e.data.substr(e.data.lastIndexOf(' ') + 1);
15
+ var salt = session_state.split('.')[1];
16
+
17
+ if (!client_id || !session_state || !salt) {
18
+ postMessage('error', e.origin);
19
+ return;
20
+ }
21
+
22
+ #{rodauth.oauth_oidc_user_agent_state_js}
23
+
24
+ // get_op_user_agent_state() is an OP defined function
25
+ // that returns the User Agent's login status at the OP.
26
+ // How it is done is entirely up to the OP.
27
+ var opuas = getOpUserAgentState();
28
+
29
+ // Here, the session_state is calculated in this particular way,
30
+ // but it is entirely up to the OP how to do it under the
31
+ // requirements defined in this specification.
32
+ var msgBuffer = new TextEncoder('utf-8').encode(client_id + ' ' + e.origin + ' ' + opuas + ' ' + salt);
33
+ crypto.subtle.digest('SHA-256', msgBuffer).then(function(hash) {
34
+ var hashArray = Array.from(new Uint8Array(hash)); // convert buffer to byte array
35
+ var hashHex = hashArray
36
+ .map(function(b) { return b.toString(16).padStart(2, "0"); })
37
+ .join("");
38
+ var ss = hashHex + "." + salt;
39
+
40
+ var stat = '';
41
+ if (session_state === ss) {
42
+ stat = 'unchanged';
43
+ } else {
44
+ stat = 'changed';
45
+ }
46
+
47
+ e.source.postMessage(stat, e.origin);
48
+ });
49
+ };
50
+
51
+ function getOpUserAgentState() {
52
+ var name = "#{rodauth.oauth_oidc_user_agent_state_cookie_key}=";
53
+ var ca = document.cookie.split(';');
54
+ var value = null;
55
+ for (var i = 0; i < ca.length; i++) {
56
+ var c = ca[i].trim();
57
+ if ((c.indexOf(name)) == 0) {
58
+ value = c.substr(name.length);
59
+ break;
60
+ }
61
+ }
62
+
63
+ return value;
64
+ }
65
+ </script>
66
+ </body>
67
+ </html>
@@ -0,0 +1,17 @@
1
+ <div class="mb-3">
2
+ <h1>#{rodauth.oauth_frontchannel_logout_redirecting_lead}</h1>
3
+ <p>
4
+ #{
5
+ rodauth.oauth_frontchannel_logout_redirecting_label(
6
+ link: "<a href=\"#{rodauth.frontchannel_logout_redirect}\">" \
7
+ "#{rodauth.oauth_frontchannel_logout_redirecting_link_label}</a>"
8
+ )
9
+ }
10
+ </p>
11
+ #{
12
+ rodauth.frontchannel_logout_urls.map do |logout_url|
13
+ "<iframe src=\"#{logout_url}\"></iframe>"
14
+ end.join
15
+ }
16
+ </div>
17
+ <meta http-equiv="refresh" content="#{rodauth.frontchannel_logout_redirect_timeout}; URL=#{rodauth.frontchannel_logout_redirect}" />
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.3.2
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-26 00:00:00.000000000 Z
11
+ date: 2023-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rodauth
@@ -72,6 +72,7 @@ extra_rdoc_files:
72
72
  - doc/release_notes/1_3_0.md
73
73
  - doc/release_notes/1_3_1.md
74
74
  - doc/release_notes/1_3_2.md
75
+ - doc/release_notes/1_4_0.md
75
76
  files:
76
77
  - CHANGELOG.md
77
78
  - LICENSE.txt
@@ -115,6 +116,7 @@ files:
115
116
  - doc/release_notes/1_3_0.md
116
117
  - doc/release_notes/1_3_1.md
117
118
  - doc/release_notes/1_3_2.md
119
+ - doc/release_notes/1_4_0.md
118
120
  - lib/generators/rodauth/oauth/install_generator.rb
119
121
  - lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
120
122
  - lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
@@ -122,6 +124,7 @@ files:
122
124
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize_error.erb
123
125
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb
124
126
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/device_verification.html.erb
127
+ - lib/generators/rodauth/oauth/templates/app/views/rodauth/frontchannel_logout.html.erb
125
128
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb
126
129
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb
127
130
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb
@@ -155,9 +158,13 @@ files:
155
158
  - lib/rodauth/features/oauth_token_introspection.rb
156
159
  - lib/rodauth/features/oauth_token_revocation.rb
157
160
  - lib/rodauth/features/oidc.rb
161
+ - lib/rodauth/features/oidc_backchannel_logout.rb
158
162
  - lib/rodauth/features/oidc_dynamic_client_registration.rb
163
+ - lib/rodauth/features/oidc_frontchannel_logout.rb
164
+ - lib/rodauth/features/oidc_logout_base.rb
159
165
  - lib/rodauth/features/oidc_rp_initiated_logout.rb
160
166
  - lib/rodauth/features/oidc_self_issued.rb
167
+ - lib/rodauth/features/oidc_session_management.rb
161
168
  - lib/rodauth/oauth.rb
162
169
  - lib/rodauth/oauth/database_extensions.rb
163
170
  - lib/rodauth/oauth/http_extensions.rb
@@ -169,10 +176,12 @@ files:
169
176
  - locales/pt.yml
170
177
  - templates/authorize.str
171
178
  - templates/authorize_error.str
179
+ - templates/check_session.str
172
180
  - templates/client_secret_field.str
173
181
  - templates/description_field.str
174
182
  - templates/device_search.str
175
183
  - templates/device_verification.str
184
+ - templates/frontchannel_logout.str
176
185
  - templates/homepage_url_field.str
177
186
  - templates/jwks_field.str
178
187
  - templates/name_field.str