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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +51 -4
- data/lib/iorwerth/hub/logout.rb +58 -0
- data/lib/iorwerth/hub/omniauth.rb +7 -4
- 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: ccb7cd07fb35bb31f30c9e1cf6745d85af73e2191f24afcbe6fd31a4a06acea4
|
|
4
|
+
data.tar.gz: 1251c0a59f7f236a19fabd563dc5ff25f2634a13c51df04bffd99bb5c0fcf00b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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/
|
|
63
|
+
provider :iorwerth_hub, name: :hub, redirect_uri: "https://podium.app/auth/hub/callback"
|
|
64
64
|
end
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
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/
|
|
14
|
+
# provider :iorwerth_hub, name: :hub, redirect_uri: "https://podium.app/auth/hub/callback"
|
|
15
15
|
# end
|
|
16
16
|
#
|
|
17
|
-
# Link to /auth/
|
|
18
|
-
# for "Start free trial": the Hub shows signup and
|
|
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
|
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.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-
|
|
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
|