iorwerth-hub 0.2.0 → 0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d030b6a6a8d09a19eabfaa5d4de8f27682e144dfb968d3c05cbcb36359a334b8
4
- data.tar.gz: 5f296810092251cb6de7e11b6edf527ba3069ffe649dd4a28d2b796d8f475641
3
+ metadata.gz: ccb7cd07fb35bb31f30c9e1cf6745d85af73e2191f24afcbe6fd31a4a06acea4
4
+ data.tar.gz: 1251c0a59f7f236a19fabd563dc5ff25f2634a13c51df04bffd99bb5c0fcf00b
5
5
  SHA512:
6
- metadata.gz: 48155d512256d8a5147036ad71acb9142d78111df2a48e46a6866d588e128d274bf02be8e3bde04f9758d05d52bdd9b28fa396c32b07ba84cfce4b72d9fddfc1
7
- data.tar.gz: 6fea37e940129be2dbc5a1907b109693dad63b867da00ebffb6fca2297502c8d882a60af19818de5d063cdd991ad804d3a321daf99c7f06e3af84d3211c9d63f
6
+ metadata.gz: c8b17b28b437e5e126fa3c37af38381fc735eff5a60446cdbd1e37da4a92f08ca5af8f6f8439e6a424acf53e33705fc4aeb7c784e26a71fa7c2144ca1bb29e21
7
+ data.tar.gz: 12fbc699b292ba772bcbb32eddca0c7c9e690758f2a60e3c99cce42600d4a90a569f9562363d69afbf726d596cf9c987280327e9d3bda2620e507b6a927d4b58
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ - Sign-in strategy forwards `organization` as an authorization parameter, so a product
6
+ that signs up organizations passes the name on its "Start free trial" link
7
+ (`/auth/hub?prompt=create&plan=…&organization=…`). The Hub creates that organization
8
+ and no longer renders a field of its own for it (platform ADR-0014).
9
+
10
+ ## 0.3.0
11
+
12
+ - `Iorwerth::Hub.end_session`: RP-initiated logout. Builds the request to the Hub's
13
+ end-session endpoint (discovered, not hard-coded) so a product's "Log out" ends the
14
+ Hub session too; POST `endpoint` + `params` to keep the ID token out of URLs, and the
15
+ Hub returns the browser to `redirect_uri` with `state` unchanged.
16
+
3
17
  ## 0.2.0
4
18
 
5
19
  - `checkout` and `portal`: Stripe Checkout and Customer Portal URLs for an organization.
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Connects a Ruby product to the Iorwerth Customer Hub. It provides four pieces:
4
4
 
5
- - **Sign-in:** the `:iorwerth_hub` OmniAuth strategy. OpenID Connect, code flow with PKCE.
5
+ - **Sign-in:** the OmniAuth strategy, mounted as `:hub`. OpenID Connect, code flow with PKCE.
6
6
  - **API client:** `Iorwerth::Hub::Client`, one method per `/api/v1` operation.
7
7
  - **Entitlement checks:** `Iorwerth::Hub.entitled?`, cached, and still answering while the Hub is down.
8
8
  - **Webhooks:** `Iorwerth::Hub::Webhook::Receiver`, a Rack endpoint that verifies each signature.
@@ -60,13 +60,20 @@ end
60
60
  require "iorwerth/hub/omniauth"
61
61
 
62
62
  Rails.application.config.middleware.use OmniAuth::Builder do
63
- provider :iorwerth_hub, redirect_uri: "https://podium.app/auth/iorwerth_hub/callback"
63
+ provider :iorwerth_hub, name: :hub, redirect_uri: "https://podium.app/auth/hub/callback"
64
64
  end
65
65
  ```
66
66
 
67
- - **Signing in:** send people to `/auth/iorwerth_hub`. The callback receives the usual OmniAuth hash.
68
- - **Starting a trial:** send people to `/auth/iorwerth_hub?prompt=create&plan=<plan key>`. The Hub
67
+ `name: :hub` mounts the strategy at `/auth/hub`, so the callback is `/auth/hub/callback`. Register
68
+ that exact URL as the product's redirect URI with the Hub.
69
+
70
+ - **Signing in:** send people to `/auth/hub`. The callback receives the usual OmniAuth hash.
71
+ - **Starting a trial:** send people to `/auth/hub?prompt=create&plan=<plan key>`. The Hub
69
72
  shows its signup page and records the requested plan.
73
+ - **Naming the organization:** if your customers are organizations (leagues, teams,
74
+ companies), collect the name in your own signup step and add `&organization=<name>` to
75
+ that trial link. The Hub creates that organization and attaches the plan to it; it never
76
+ asks for the name itself. Leave it off for individual customers.
70
77
 
71
78
  The callback's `uid` is the Hub's `usr_` id. `info` holds the person's name and email.
72
79
  `Iorwerth::Hub.organizations(auth)` returns the organizations they belong to, as this
@@ -86,6 +93,46 @@ def callback
86
93
  end
87
94
  ```
88
95
 
96
+ ## Signing out
97
+
98
+ "Log out" must end the Hub session too, or the next sign-in is silent. Clear your own
99
+ session first, then send the browser to the Hub's end-session endpoint. POST it so the ID
100
+ token stays out of URLs; the Hub returns to `post_logout_redirect_uri` with `state`
101
+ unchanged, which you can use to pick where to land.
102
+
103
+ The POST is cross-site, so the browser does not send the Hub's session cookie with it. The
104
+ Hub handles that for you: it bounces the browser through a same-site redirect of its own that
105
+ does carry the cookie, then on to `post_logout_redirect_uri`. Your side is unchanged — submit
106
+ the form and let the browser follow the redirects.
107
+
108
+ ```ruby
109
+ # config/initializers/omniauth.rb — register the return URL with the Hub, per product:
110
+ # post_logout_redirect_uris = https://podium.app/auth/hub/signed_out
111
+
112
+ def destroy # DELETE /logout
113
+ id_token = session[:hub_id_token] # saved from auth.credentials.id_token at sign-in
114
+ reset_session # end the product's own session first
115
+ @logout = Iorwerth::Hub.end_session(
116
+ id_token: id_token,
117
+ redirect_uri: signed_out_url, # must be one of the registered post-logout URIs
118
+ state: SecureRandom.hex(8)
119
+ )
120
+ render :signing_out # a view with an auto-submitting POST form
121
+ end
122
+ ```
123
+
124
+ ```erb
125
+ <%# app/views/sessions/signing_out.html.erb %>
126
+ <%= form_tag @logout.endpoint, method: :post, id: "hub-logout" do %>
127
+ <% @logout.params.each do |name, value| %><%= hidden_field_tag name, value %><% end %>
128
+ <% end %>
129
+ <script>document.getElementById("hub-logout").submit()</script>
130
+ ```
131
+
132
+ `end_session` discovers the endpoint from the Hub (it does not hard-code it). When a plain
133
+ redirect is acceptable and the ID token in the URL is not a concern, `@logout.redirect_url`
134
+ is the endpoint with the fields in the query string instead.
135
+
89
136
  ## API
90
137
 
91
138
  ```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
@@ -11,11 +11,14 @@ module OmniAuth
11
11
  # # config/initializers/omniauth.rb
12
12
  # require "iorwerth/hub/omniauth"
13
13
  # Rails.application.config.middleware.use OmniAuth::Builder do
14
- # provider :iorwerth_hub, redirect_uri: "https://podium.app/auth/iorwerth_hub/callback"
14
+ # provider :iorwerth_hub, name: :hub, redirect_uri: "https://podium.app/auth/hub/callback"
15
15
  # end
16
16
  #
17
- # Link to /auth/iorwerth_hub to sign in, or /auth/iorwerth_hub?prompt=create&plan=<key>
18
- # for "Start free trial": the Hub shows signup and records the plan. The auth hash's
17
+ # `name: :hub` mounts it at /auth/hub. Link to /auth/hub to sign in, or
18
+ # /auth/hub?prompt=create&plan=<key> for "Start free trial": the Hub shows signup and
19
+ # records the plan. When your customers are organizations, collect the organization's
20
+ # name in your own signup wizard and pass it as &organization=<name>; the Hub creates
21
+ # that organization and never shows a field of its own for it. The auth hash's
19
22
  # extra.raw_info carries the `organizations` claim (Iorwerth::Hub.organizations(auth)).
20
23
  class IorwerthHub < OpenIDConnect
21
24
  option :name, "iorwerth_hub"
@@ -23,7 +26,7 @@ module OmniAuth
23
26
  option :pkce, true
24
27
  option :response_type, "code"
25
28
  option :scope, %i[openid email profile]
26
- option :allow_authorize_params, %i[plan]
29
+ option :allow_authorize_params, %i[plan organization]
27
30
 
28
31
  def initialize(app, *args, &block)
29
32
  super
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Iorwerth
4
4
  module Hub
5
- VERSION = "0.2.0"
5
+ VERSION = "0.4.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.4.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-17 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