iorwerth-hub 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d030b6a6a8d09a19eabfaa5d4de8f27682e144dfb968d3c05cbcb36359a334b8
4
- data.tar.gz: 5f296810092251cb6de7e11b6edf527ba3069ffe649dd4a28d2b796d8f475641
3
+ metadata.gz: cb15506bb31b75a41adcff9d816410edfbb6386f73046d972ad2588b0974a500
4
+ data.tar.gz: ef02264de56eb7fa73b6210b4a2f017ab83b94ba114c58ba35355d84ce9e60d2
5
5
  SHA512:
6
- metadata.gz: 48155d512256d8a5147036ad71acb9142d78111df2a48e46a6866d588e128d274bf02be8e3bde04f9758d05d52bdd9b28fa396c32b07ba84cfce4b72d9fddfc1
7
- data.tar.gz: 6fea37e940129be2dbc5a1907b109693dad63b867da00ebffb6fca2297502c8d882a60af19818de5d063cdd991ad804d3a321daf99c7f06e3af84d3211c9d63f
6
+ metadata.gz: 65995baaa899db6097a4997a86acad674b3d3272166fa8055ac558c69738eb171a99edb99cace0fa93496b99b05a6af252ebe6f1c1d3c5b90f5e9a7c0be64d66
7
+ data.tar.gz: 72f260c64686c7c0e92edfc16d704b1060106f4c467a5dd771adccbc8c08f1677d17a14ad61d792869d37af1d8ce22dee00a25185390200b6d66ef804479e405
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - `Iorwerth::Hub.end_session`: RP-initiated logout. Builds the request to the Hub's
6
+ end-session endpoint (discovered, not hard-coded) so a product's "Log out" ends the
7
+ Hub session too; POST `endpoint` + `params` to keep the ID token out of URLs, and the
8
+ Hub returns the browser to `redirect_uri` with `state` unchanged.
9
+
3
10
  ## 0.2.0
4
11
 
5
12
  - `checkout` and `portal`: Stripe Checkout and Customer Portal URLs for an organization.
data/README.md CHANGED
@@ -86,6 +86,41 @@ def callback
86
86
  end
87
87
  ```
88
88
 
89
+ ## Signing out
90
+
91
+ "Log out" must end the Hub session too, or the next sign-in is silent. Clear your own
92
+ session first, then send the browser to the Hub's end-session endpoint. POST it so the ID
93
+ token stays out of URLs; the Hub returns to `post_logout_redirect_uri` with `state`
94
+ unchanged, which you can use to pick where to land.
95
+
96
+ ```ruby
97
+ # config/initializers/omniauth.rb — register the return URL with the Hub, per product:
98
+ # post_logout_redirect_uris = https://podium.app/auth/hub/signed_out
99
+
100
+ def destroy # DELETE /logout
101
+ id_token = session[:hub_id_token] # saved from auth.credentials.id_token at sign-in
102
+ reset_session # end the product's own session first
103
+ @logout = Iorwerth::Hub.end_session(
104
+ id_token: id_token,
105
+ redirect_uri: signed_out_url, # must be one of the registered post-logout URIs
106
+ state: SecureRandom.hex(8)
107
+ )
108
+ render :signing_out # a view with an auto-submitting POST form
109
+ end
110
+ ```
111
+
112
+ ```erb
113
+ <%# app/views/sessions/signing_out.html.erb %>
114
+ <%= form_tag @logout.endpoint, method: :post, id: "hub-logout" do %>
115
+ <% @logout.params.each do |name, value| %><%= hidden_field_tag name, value %><% end %>
116
+ <% end %>
117
+ <script>document.getElementById("hub-logout").submit()</script>
118
+ ```
119
+
120
+ `end_session` discovers the endpoint from the Hub (it does not hard-code it). When a plain
121
+ redirect is acceptable and the ID token in the URL is not a concern, `@logout.redirect_url`
122
+ is the endpoint with the fields in the query string instead.
123
+
89
124
  ## API
90
125
 
91
126
  ```ruby
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "openssl"
6
+ require "uri"
7
+
8
+ module Iorwerth
9
+ module Hub
10
+ # RP-initiated logout (OpenID Connect RP-Initiated Logout 1.0): end the Hub session when a
11
+ # product signs someone out. Clear the product's own session first, then send the browser
12
+ # to the Hub's end-session endpoint. POST it (`endpoint` + `params`) so the ID token stays
13
+ # out of URLs and server logs; the Hub returns the browser to `redirect_uri` with `state`
14
+ # unchanged, so a product can carry its own return target through the round trip.
15
+ #
16
+ # logout = Iorwerth::Hub.end_session(id_token: session[:hub_id_token],
17
+ # redirect_uri: signed_out_url, state: SecureRandom.hex)
18
+ # # an auto-submitting POST form in the view:
19
+ # form_tag logout.endpoint, method: :post do
20
+ # safe_join(logout.params.map { |name, value| hidden_field_tag(name, value) })
21
+ # end
22
+ class EndSession
23
+ attr_reader :id_token, :redirect_uri, :state, :client_id
24
+
25
+ def initialize(id_token:, redirect_uri: nil, state: nil,
26
+ client_id: Hub.config.oidc_client_id, url: Hub.config.url)
27
+ @id_token = id_token
28
+ @redirect_uri = redirect_uri
29
+ @state = state
30
+ @client_id = client_id
31
+ @url = url
32
+ end
33
+
34
+ # The Hub's end-session endpoint, from its discovery document.
35
+ def endpoint
36
+ Hub.discovery(@url).fetch("end_session_endpoint")
37
+ end
38
+
39
+ # The logout request's fields, for a POST form. Absent ones are dropped.
40
+ def params
41
+ {
42
+ "id_token_hint" => id_token,
43
+ "client_id" => client_id,
44
+ "post_logout_redirect_uri" => redirect_uri,
45
+ "state" => state
46
+ }.compact
47
+ end
48
+
49
+ # The endpoint with the fields in the query string, for a plain redirect. Prefer a POST
50
+ # of `endpoint` + `params` when the ID token should not appear in a URL.
51
+ def redirect_url
52
+ uri = URI(endpoint)
53
+ uri.query = URI.encode_www_form(params)
54
+ uri.to_s
55
+ end
56
+ end
57
+ end
58
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Iorwerth
4
4
  module Hub
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
data/lib/iorwerth/hub.rb CHANGED
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+ require "net/http"
5
+ require "openssl"
6
+ require "uri"
7
+
3
8
  require "iorwerth/hub/version"
4
9
  require "iorwerth/hub/errors"
5
10
  require "iorwerth/hub/configuration"
6
11
  require "iorwerth/hub/client"
7
12
  require "iorwerth/hub/entitlements"
13
+ require "iorwerth/hub/logout"
8
14
  require "iorwerth/hub/webhook"
9
15
 
10
16
  module Iorwerth
@@ -48,9 +54,38 @@ module Iorwerth
48
54
  info.fetch("organizations", info.fetch(:organizations, []))
49
55
  end
50
56
 
57
+ # Build an RP-initiated logout for one person's session. See EndSession.
58
+ def end_session(id_token:, redirect_uri: nil, state: nil)
59
+ EndSession.new(id_token: id_token, redirect_uri: redirect_uri, state: state)
60
+ end
61
+
62
+ # The Hub's OpenID Connect discovery document, fetched once per URL and cached.
63
+ def discovery(url = config.url)
64
+ raise ConfigurationError, "Set HUB_URL." if url.nil? || url.empty?
65
+
66
+ (@discovery ||= {})[url] ||= fetch_discovery(url)
67
+ end
68
+
51
69
  # Forget settings and cached state; for tests.
52
70
  def reset!
53
- @config = @client = @entitlements = nil
71
+ @config = @client = @entitlements = @discovery = nil
72
+ end
73
+
74
+ private
75
+
76
+ def fetch_discovery(url)
77
+ uri = URI("#{url.chomp('/')}/.well-known/openid-configuration")
78
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
79
+ open_timeout: config.open_timeout,
80
+ read_timeout: config.timeout) do |http|
81
+ request = Net::HTTP::Get.new(uri)
82
+ request["Accept"] = "application/json"
83
+ request["User-Agent"] = "iorwerth-hub-ruby/#{VERSION}"
84
+ http.request(request)
85
+ end
86
+ JSON.parse(response.body)
87
+ rescue SocketError, SystemCallError, Timeout::Error, IOError, OpenSSL::SSL::SSLError => e
88
+ raise ConnectionError, "#{uri.host}: #{e.class}: #{e.message}"
54
89
  end
55
90
  end
56
91
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iorwerth-hub
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Iorwerth Technologies
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-08 00:00:00.000000000 Z
11
+ date: 2026-10-10 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Sign-in through the Hub (OmniAuth), its /api/v1 client, cached entitlement
14
14
  checks, and a Rack endpoint that verifies the Hub's webhooks.
@@ -25,6 +25,7 @@ files:
25
25
  - lib/iorwerth/hub/configuration.rb
26
26
  - lib/iorwerth/hub/entitlements.rb
27
27
  - lib/iorwerth/hub/errors.rb
28
+ - lib/iorwerth/hub/logout.rb
28
29
  - lib/iorwerth/hub/omniauth.rb
29
30
  - lib/iorwerth/hub/version.rb
30
31
  - lib/iorwerth/hub/webhook.rb