rodauth-oauth 1.3.2 → 1.4.0

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