rodauth-oauth 1.3.1 → 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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -12
  3. data/doc/release_notes/1_3_2.md +14 -0
  4. data/doc/release_notes/1_4_0.md +49 -0
  5. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize.html.erb +23 -23
  6. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/frontchannel_logout.html.erb +10 -0
  7. data/lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_applications.html.erb +1 -1
  8. data/lib/generators/rodauth/oauth/templates/db/migrate/create_rodauth_oauth.rb +21 -0
  9. data/lib/generators/rodauth/oauth/views_generator.rb +2 -2
  10. data/lib/rodauth/features/oauth_application_management.rb +1 -1
  11. data/lib/rodauth/features/oauth_assertion_base.rb +1 -1
  12. data/lib/rodauth/features/oauth_authorize_base.rb +1 -1
  13. data/lib/rodauth/features/oauth_base.rb +31 -30
  14. data/lib/rodauth/features/oauth_device_code_grant.rb +2 -2
  15. data/lib/rodauth/features/oauth_dynamic_client_registration.rb +9 -0
  16. data/lib/rodauth/features/oauth_grant_management.rb +1 -1
  17. data/lib/rodauth/features/oauth_jwt.rb +3 -3
  18. data/lib/rodauth/features/oauth_jwt_base.rb +23 -12
  19. data/lib/rodauth/features/oauth_jwt_bearer_grant.rb +1 -1
  20. data/lib/rodauth/features/oauth_jwt_jwks.rb +1 -1
  21. data/lib/rodauth/features/oauth_jwt_secured_authorization_request.rb +25 -6
  22. data/lib/rodauth/features/oauth_pushed_authorization_request.rb +33 -24
  23. data/lib/rodauth/features/oauth_resource_server.rb +1 -1
  24. data/lib/rodauth/features/oauth_saml_bearer_grant.rb +79 -47
  25. data/lib/rodauth/features/oauth_tls_client_auth.rb +1 -1
  26. data/lib/rodauth/features/oauth_token_introspection.rb +1 -1
  27. data/lib/rodauth/features/oauth_token_revocation.rb +1 -1
  28. data/lib/rodauth/features/oidc.rb +27 -8
  29. data/lib/rodauth/features/oidc_backchannel_logout.rb +120 -0
  30. data/lib/rodauth/features/oidc_dynamic_client_registration.rb +25 -0
  31. data/lib/rodauth/features/oidc_frontchannel_logout.rb +134 -0
  32. data/lib/rodauth/features/oidc_logout_base.rb +76 -0
  33. data/lib/rodauth/features/oidc_rp_initiated_logout.rb +29 -6
  34. data/lib/rodauth/features/oidc_session_management.rb +89 -0
  35. data/lib/rodauth/oauth/http_extensions.rb +1 -1
  36. data/lib/rodauth/oauth/version.rb +1 -1
  37. data/locales/en.yml +9 -0
  38. data/locales/pt.yml +9 -0
  39. data/templates/check_session.str +67 -0
  40. data/templates/frontchannel_logout.str +17 -0
  41. metadata +13 -2
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth"
4
+
5
+ # :nocov:
6
+ raise LoadError, "the `:oidc_frontchannel_logout` requires rodauth 2.32.0 or higher" if Rodauth::VERSION < "2.32.0"
7
+
8
+ # :nocov:
9
+
10
+ module Rodauth
11
+ Feature.define(:oidc_frontchannel_logout, :OidFrontchannelLogout) do
12
+ depends :logout, :oidc_logout_base
13
+
14
+ view "frontchannel_logout", "Logout", "frontchannel_logout"
15
+
16
+ translatable_method :oauth_frontchannel_logout_redirecting_lead, "You are being redirected..."
17
+ translatable_method :oauth_frontchannel_logout_redirecting_label, "please click %<link>s if your browser does not " \
18
+ "redirect you in a few seconds."
19
+ translatable_method :oauth_frontchannel_logout_redirecting_link_label, "here"
20
+ auth_value_method :frontchannel_logout_session_supported, true
21
+ auth_value_method :frontchannel_logout_redirect_timeout, 5
22
+ auth_value_method :oauth_applications_frontchannel_logout_uri_column, :frontchannel_logout_uri
23
+ auth_value_method :oauth_applications_frontchannel_logout_session_required_column, :frontchannel_logout_session_required
24
+
25
+ attr_reader :frontchannel_logout_urls
26
+
27
+ attr_reader :frontchannel_logout_redirect
28
+
29
+ def logout
30
+ @visited_sites = session[visited_sites_key]
31
+
32
+ super
33
+ end
34
+
35
+ def _logout_response
36
+ visited_sites = @visited_sites
37
+
38
+ return super unless visited_sites
39
+
40
+ logout_urls = db[oauth_applications_table]
41
+ .where(oauth_applications_client_id_column => visited_sites.map(&:first))
42
+ .as_hash(oauth_applications_client_id_column, oauth_applications_frontchannel_logout_uri_column)
43
+
44
+ return super if logout_urls.empty?
45
+
46
+ generate_frontchannel_logout_urls(visited_sites, logout_urls)
47
+
48
+ @frontchannel_logout_redirect = logout_redirect
49
+
50
+ set_notice_flash logout_notice_flash
51
+ return_response frontchannel_logout_view
52
+ end
53
+
54
+ # overrides rp-initiate logout response
55
+ def _oidc_logout_response
56
+ visited_sites = @visited_sites
57
+
58
+ return super unless visited_sites
59
+
60
+ logout_urls = db[oauth_applications_table]
61
+ .where(oauth_applications_client_id_column => visited_sites.map(&:first))
62
+ .as_hash(oauth_applications_client_id_column, oauth_applications_frontchannel_logout_uri_column)
63
+
64
+ return super if logout_urls.empty?
65
+
66
+ generate_frontchannel_logout_urls(visited_sites, logout_urls)
67
+
68
+ @frontchannel_logout_redirect = oidc_logout_redirect
69
+
70
+ set_notice_flash logout_notice_flash
71
+ return_response frontchannel_logout_view
72
+ end
73
+
74
+ private
75
+
76
+ def generate_frontchannel_logout_urls(visited_sites, logout_urls)
77
+ @frontchannel_logout_urls = logout_urls.flat_map do |client_id, logout_url|
78
+ next unless logout_url
79
+
80
+ sids = visited_sites.select { |cid, _| cid == client_id }.map(&:last)
81
+
82
+ sids.map do |sid|
83
+ logout_url = URI(logout_url)
84
+
85
+ if sid
86
+ query = logout_url.query
87
+ query = if query
88
+ URI.decode_www_form(query)
89
+ else
90
+ []
91
+ end
92
+ query << ["iss", oauth_jwt_issuer]
93
+ query << ["sid", sid]
94
+ logout_url.query = URI.encode_www_form(query)
95
+ end
96
+
97
+ logout_url
98
+ end
99
+ end.compact
100
+ end
101
+
102
+ def id_token_claims(oauth_grant, signing_algorithm)
103
+ claims = super
104
+
105
+ return claims unless oauth_application[oauth_applications_frontchannel_logout_uri_column]
106
+
107
+ session_id_in_claims(oauth_grant, claims)
108
+
109
+ claims
110
+ end
111
+
112
+ def should_set_oauth_application_in_visited_sites?
113
+ true
114
+ end
115
+
116
+ def should_set_sid_in_visited_sites?(oauth_application)
117
+ super || requires_frontchannel_logout_session?(oauth_application)
118
+ end
119
+
120
+ def requires_frontchannel_logout_session?(oauth_application)
121
+ (
122
+ oauth_application &&
123
+ oauth_application[oauth_applications_frontchannel_logout_session_required_column]
124
+ ) || frontchannel_logout_session_supported
125
+ end
126
+
127
+ def oauth_server_metadata_body(*)
128
+ super.tap do |data|
129
+ data[:frontchannel_logout_supported] = true
130
+ data[:frontchannel_logout_session_supported] = frontchannel_logout_session_supported
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rodauth/oauth"
4
+
5
+ module Rodauth
6
+ Feature.define(:oidc_logout_base, :OidcLogoutBase) do
7
+ depends :oidc
8
+
9
+ session_key :visited_sites_key, :visited_sites
10
+
11
+ private
12
+
13
+ # set application/sid in visited sites when required
14
+ def create_oauth_grant(create_params = {})
15
+ sid_in_visited_sites
16
+
17
+ super
18
+ end
19
+
20
+ def active_sessions?(session_id)
21
+ !active_sessions_ds.where(active_sessions_session_id_column => session_id).empty?
22
+ end
23
+
24
+ def session_id_in_claims(oauth_grant, claims)
25
+ oauth_application_in_visited_sites do
26
+ if should_set_sid_in_visited_sites?(oauth_application)
27
+ # id_token or token response types
28
+ session_id = if (sess = session[session_id_session_key])
29
+ compute_hmac(sess)
30
+ else
31
+ # code response type
32
+ ds = db[active_sessions_table]
33
+ ds = ds.where(active_sessions_account_id_column => oauth_grant[oauth_grants_account_id_column])
34
+ ds = ds.order(Sequel.desc(active_sessions_last_use_column))
35
+ ds.get(active_sessions_session_id_column)
36
+ end
37
+
38
+ claims[:sid] = session_id
39
+ end
40
+ end
41
+ end
42
+
43
+ def oauth_application_in_visited_sites
44
+ visited_sites = session[visited_sites_key] || []
45
+
46
+ session_id = yield
47
+
48
+ visited_site = [oauth_application[oauth_applications_client_id_column], session_id]
49
+
50
+ return if visited_sites.include?(visited_site)
51
+
52
+ visited_sites << visited_site
53
+ set_session_value(visited_sites_key, visited_sites)
54
+ end
55
+
56
+ def sid_in_visited_sites
57
+ return unless should_set_oauth_application_in_visited_sites?
58
+
59
+ oauth_application_in_visited_sites do
60
+ if should_set_sid_in_visited_sites?(oauth_application)
61
+ ds = active_sessions_ds.order(Sequel.desc(active_sessions_last_use_column))
62
+
63
+ ds.get(active_sessions_session_id_column)
64
+ end
65
+ end
66
+ end
67
+
68
+ def should_set_oauth_application_in_visited_sites?
69
+ false
70
+ end
71
+
72
+ def should_set_sid_in_visited_sites?(*)
73
+ false
74
+ end
75
+ end
76
+ end
@@ -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.1"
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.1
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-06-27 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
@@ -71,6 +71,8 @@ extra_rdoc_files:
71
71
  - doc/release_notes/1_2_0.md
72
72
  - doc/release_notes/1_3_0.md
73
73
  - doc/release_notes/1_3_1.md
74
+ - doc/release_notes/1_3_2.md
75
+ - doc/release_notes/1_4_0.md
74
76
  files:
75
77
  - CHANGELOG.md
76
78
  - LICENSE.txt
@@ -113,6 +115,8 @@ files:
113
115
  - doc/release_notes/1_2_0.md
114
116
  - doc/release_notes/1_3_0.md
115
117
  - doc/release_notes/1_3_1.md
118
+ - doc/release_notes/1_3_2.md
119
+ - doc/release_notes/1_4_0.md
116
120
  - lib/generators/rodauth/oauth/install_generator.rb
117
121
  - lib/generators/rodauth/oauth/templates/app/models/oauth_application.rb
118
122
  - lib/generators/rodauth/oauth/templates/app/models/oauth_grant.rb
@@ -120,6 +124,7 @@ files:
120
124
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/authorize_error.erb
121
125
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/device_search.html.erb
122
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
123
128
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/new_oauth_application.html.erb
124
129
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application.html.erb
125
130
  - lib/generators/rodauth/oauth/templates/app/views/rodauth/oauth_application_oauth_grants.html.erb
@@ -153,9 +158,13 @@ files:
153
158
  - lib/rodauth/features/oauth_token_introspection.rb
154
159
  - lib/rodauth/features/oauth_token_revocation.rb
155
160
  - lib/rodauth/features/oidc.rb
161
+ - lib/rodauth/features/oidc_backchannel_logout.rb
156
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
157
165
  - lib/rodauth/features/oidc_rp_initiated_logout.rb
158
166
  - lib/rodauth/features/oidc_self_issued.rb
167
+ - lib/rodauth/features/oidc_session_management.rb
159
168
  - lib/rodauth/oauth.rb
160
169
  - lib/rodauth/oauth/database_extensions.rb
161
170
  - lib/rodauth/oauth/http_extensions.rb
@@ -167,10 +176,12 @@ files:
167
176
  - locales/pt.yml
168
177
  - templates/authorize.str
169
178
  - templates/authorize_error.str
179
+ - templates/check_session.str
170
180
  - templates/client_secret_field.str
171
181
  - templates/description_field.str
172
182
  - templates/device_search.str
173
183
  - templates/device_verification.str
184
+ - templates/frontchannel_logout.str
174
185
  - templates/homepage_url_field.str
175
186
  - templates/jwks_field.str
176
187
  - templates/name_field.str