iorwerth-hub 0.1.1 → 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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +40 -1
- data/lib/iorwerth/hub/client.rb +15 -0
- data/lib/iorwerth/hub/logout.rb +58 -0
- data/lib/iorwerth/hub/version.rb +1 -1
- data/lib/iorwerth/hub.rb +36 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cb15506bb31b75a41adcff9d816410edfbb6386f73046d972ad2588b0974a500
|
|
4
|
+
data.tar.gz: ef02264de56eb7fa73b6210b4a2f017ab83b94ba114c58ba35355d84ce9e60d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 65995baaa899db6097a4997a86acad674b3d3272166fa8055ac558c69738eb171a99edb99cace0fa93496b99b05a6af252ebe6f1c1d3c5b90f5e9a7c0be64d66
|
|
7
|
+
data.tar.gz: 72f260c64686c7c0e92edfc16d704b1060106f4c467a5dd771adccbc8c08f1677d17a14ad61d792869d37af1d8ce22dee00a25185390200b6d66ef804479e405
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
10
|
+
## 0.2.0
|
|
11
|
+
|
|
12
|
+
- `checkout` and `portal`: Stripe Checkout and Customer Portal URLs for an organization.
|
|
13
|
+
`checkout` without `plan:` buys the plan the organization requested at signup.
|
|
14
|
+
- `list_plans` answers no longer carry `ends_on`, and `interval` is `month` or `year`:
|
|
15
|
+
the Hub has no seasonal plans (ADR-0010).
|
|
16
|
+
|
|
3
17
|
## 0.1.1
|
|
4
18
|
|
|
5
19
|
- Sign-in works against a Hub on plain http, such as the local one from
|
data/README.md
CHANGED
|
@@ -77,13 +77,50 @@ def callback
|
|
|
77
77
|
auth = request.env["omniauth.auth"]
|
|
78
78
|
user = User.find_or_initialize_by(hub_id: auth.uid)
|
|
79
79
|
user.update!(email: auth.info.email, name: auth.info.name)
|
|
80
|
-
|
|
80
|
+
# Each has "type": "business" or "personal". A product whose customers are
|
|
81
|
+
# organizations provisions business ones; one for individuals, the personal one.
|
|
82
|
+
Iorwerth::Hub.organizations(auth).select { |o| o["type"] == "business" }.each do |organization|
|
|
81
83
|
tenant = Tenant.find_or_create_by!(hub_organization_id: organization["id"])
|
|
82
84
|
# ...
|
|
83
85
|
end
|
|
84
86
|
end
|
|
85
87
|
```
|
|
86
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
|
+
|
|
87
124
|
## API
|
|
88
125
|
|
|
89
126
|
```ruby
|
|
@@ -91,6 +128,8 @@ hub = Iorwerth::Hub.client
|
|
|
91
128
|
hub.get_organization("org_01J...") # profile, members, subscription, entitlements
|
|
92
129
|
hub.report_tenant("org_01J...", external_tenant_id: tenant.id.to_s)
|
|
93
130
|
hub.report_usage("org_01J...", usage: { teams: 7 })
|
|
131
|
+
hub.checkout("org_01J...", plan: "pro", success_url: done_url, cancel_url: billing_url)["url"]
|
|
132
|
+
hub.portal("org_01J...", return_url: billing_url)["url"] # "Manage billing"
|
|
94
133
|
hub.invite("org_01J...", email: "sam@example.com", role: "member")
|
|
95
134
|
hub.each_organization { |organization| ... } # every organization this product can see
|
|
96
135
|
hub.each_event(since: last_event_id) { |event| ... } # catch up after downtime
|
data/lib/iorwerth/hub/client.rb
CHANGED
|
@@ -23,6 +23,8 @@ module Iorwerth
|
|
|
23
23
|
"get_organization" => [:get, "/api/v1/organizations/{organization_id}"],
|
|
24
24
|
"get_entitlements" => [:get, "/api/v1/organizations/{organization_id}/entitlements"],
|
|
25
25
|
"report_tenant" => [:post, "/api/v1/organizations/{organization_id}/tenant"],
|
|
26
|
+
"checkout" => [:post, "/api/v1/organizations/{organization_id}/checkout"],
|
|
27
|
+
"portal" => [:post, "/api/v1/organizations/{organization_id}/portal"],
|
|
26
28
|
"get_usage" => [:get, "/api/v1/organizations/{organization_id}/usage"],
|
|
27
29
|
"report_usage" => [:post, "/api/v1/organizations/{organization_id}/usage"],
|
|
28
30
|
"invite" => [:post, "/api/v1/organizations/{organization_id}/invites"],
|
|
@@ -74,6 +76,19 @@ module Iorwerth
|
|
|
74
76
|
idempotency_key: idempotency_key)
|
|
75
77
|
end
|
|
76
78
|
|
|
79
|
+
# A Stripe Checkout page for one of this product's plans; without plan:, the one requested
|
|
80
|
+
# at signup. Redirect the customer to answer["url"].
|
|
81
|
+
def checkout(organization_id, success_url:, cancel_url:, plan: nil, idempotency_key: SecureRandom.uuid)
|
|
82
|
+
body = { plan: plan, success_url: success_url, cancel_url: cancel_url }.compact
|
|
83
|
+
call("checkout", organization_id: organization_id, body: body, idempotency_key: idempotency_key)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# A Stripe Customer Portal page: card, invoices, plan changes, cancellation.
|
|
87
|
+
def portal(organization_id, return_url:, idempotency_key: SecureRandom.uuid)
|
|
88
|
+
call("portal", organization_id: organization_id, body: { return_url: return_url },
|
|
89
|
+
idempotency_key: idempotency_key)
|
|
90
|
+
end
|
|
91
|
+
|
|
77
92
|
def get_usage(organization_id)
|
|
78
93
|
call("get_usage", organization_id: organization_id)
|
|
79
94
|
end
|
|
@@ -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
|
data/lib/iorwerth/hub/version.rb
CHANGED
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.
|
|
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-
|
|
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
|