kiosk-user-idp-devise 0.5.20

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: eaa5961003f556d23c16fae8b4715be095b5b75dcf922a4e45965789c0bfc9e5
4
+ data.tar.gz: c9ca748caede6c73d2582e2872a98a6e1631a2a4b2211c6dd1a80dc1fd7846c9
5
+ SHA512:
6
+ metadata.gz: 21e4d2e6b82c2ff6ce89e8a7c201fb8f634c6f9562773a070719d28f39df2a6b8ab38297e27f34875b6d63efce9c51d4a2571b74296096ca07663bc8478c335c
7
+ data.tar.gz: a79089492e3efec13c759bd13ee2159970ba102aba9dd61289b449282c21a36d825881f400c6836313ab622efdbdccbb2f1a1e6869a6d46c6f43da0e84e7ce58
data/CHANGELOG.md ADDED
@@ -0,0 +1,122 @@
1
+ # Changelog
2
+
3
+ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).
4
+
5
+ How to write an entry, and what a release section means: `CHANGELOG-RULE.md` at
6
+ the root of this repository —
7
+ <https://github.com/kiosk-hq/kiosk/blob/main/CHANGELOG-RULE.md>. In short: under
8
+ 200 characters, one or two sentences, the essence rather than the content; write it
9
+ under `## [Unreleased]`; a cut renames that heading to
10
+ `## [MAJOR.MINOR.PATCH] — <date>` and opens a fresh empty one above it; nothing
11
+ already written is edited.
12
+
13
+ ## [Unreleased]
14
+
15
+ ## [0.5.20] — 2026-10-20
16
+
17
+ - 2026-10-20: Version 0.5.20, the tree cut that matches skill 0.5.20.
18
+
19
+ ## [0.5.19] — 2026-10-20
20
+
21
+ - 2026-10-19: `DeviseSession` connects to an origin that names an IPv6 literal, such as `http://[::1]:3000`.
22
+
23
+ ## [0.5.18] — 2026-10-18
24
+
25
+ - 2026-10-18: Version 0.5.18, the tree cut that matches skill 0.5.18.
26
+
27
+ ## [0.5.17] — 2026-10-18
28
+
29
+ - 2026-10-18: Version 0.5.17, the tree cut that matches skill 0.5.17.
30
+
31
+ ## [0.5.16] — 2026-10-18
32
+
33
+ - 2026-10-18: `Kiosk::UserIdentityProviders::DeviseSession#sign_out!` is removed.
34
+ ## [0.5.15] — 2026-10-18
35
+
36
+ - The README names the human-side page gate and the link endpoints as the callers of the adapter.
37
+
38
+ ## [0.5.14] — 2026-10-18
39
+
40
+ - 2026-10-18: Version 0.5.14, the tree cut that matches skill 0.5.14.
41
+
42
+ - 2026-10-17: `user_id_column` is a setting of this gem, default `:id`; `Kiosk.configure` sets it as before.
43
+
44
+ ## [0.5.13] — 2026-10-11
45
+
46
+ - 2026-10-11: Version 0.5.13, the tree cut that matches skill 0.5.13.
47
+
48
+ - `WireSignpost` and `SessionsController` answer a JSON caller at the Devise sign-in and sign-out pages with a pointer to `/.well-known/kiosk.json`, so an operator need not write them.
49
+
50
+ ## [0.5.12] — 2026-10-08
51
+
52
+ - 2026-10-08: Version 0.5.12, the tree cut that matches skill 0.5.12; this gem's surface is unchanged.
53
+
54
+ ## [0.5.11] — 2026-10-08
55
+
56
+ - 2026-10-08: Version 0.5.11, the tree cut that matches skill 0.5.11; this gem's surface is unchanged.
57
+
58
+ ## [0.5.10] — 2026-10-08
59
+
60
+ - 2026-10-08: Version 0.5.10, the tree cut that matches skill 0.5.10; this gem's surface is unchanged.
61
+
62
+ ## [0.5.9] — 2026-10-08
63
+
64
+ - 2026-10-08: Version 0.5.9, the tree cut that matches skill 0.5.9; this gem's surface is unchanged.
65
+
66
+ ## [0.5.8] — 2026-10-07
67
+
68
+ - 2026-10-07: Version 0.5.8, the tree cut that matches skill 0.5.8; this gem's surface is unchanged.
69
+
70
+ ## [0.5.7] — 2026-10-07
71
+
72
+ - 2026-10-07: Version 0.5.7, the tree cut that matches skill 0.5.7; this gem's surface is unchanged.
73
+
74
+ ## [0.5.6] — 2026-10-07
75
+
76
+ - 2026-10-07: Version 0.5.6, the tree cut that matches skill 0.5.6; this gem's surface is unchanged.
77
+
78
+ ## [0.5.5] — 2026-10-07
79
+
80
+ - 2026-10-07: Version 0.5.5, the tree cut that matches skill 0.5.5; this gem's surface is unchanged.
81
+
82
+ ## [0.5.4] — 2026-10-07
83
+
84
+ - 2026-10-07: Version 0.5.4, the tree cut that matches skill 0.5.4; this gem's surface is unchanged.
85
+
86
+ ## [0.5.3] — 2026-10-06
87
+
88
+ - 2026-10-06: Version 0.5.3, the tree cut that matches skill 0.5.3; this gem's surface is unchanged.
89
+
90
+ ## [0.5.2] — 2026-10-06
91
+
92
+ - 2026-10-06: Version 0.5.2, the tree cut that matches skill 0.5.2; this gem's surface is unchanged.
93
+
94
+ ## [0.5.0] — 2026-09-26
95
+
96
+ ### Changed
97
+
98
+ - **`DeviseSession` reads TLS off the target's scheme.** It opened every socket in cleartext, so an `https://` server URL could not be dialled at all.
99
+
100
+ - README: every `gem` line in the install section now carries `github: "kiosk-hq/kiosk"`, so a copied line resolves before publication; the banner above them states what the block does rather than asking the reader to add it.
101
+
102
+ ### Added
103
+
104
+ - `Kiosk::UserIdentityProviders::DeviseSession` (`require "kiosk/user_identity_providers/devise_session"`) — the CLIENT end of this adapter. `Devise` reads the Warden user off an incoming request; this drives the shipped Devise form to create the session that read finds: GET `/users/sign_in` for the cookie and the CSRF token, POST the credentials, keep the Set-Cookie. `session: true` is the only knob, and it is opt-in per request so an agent's Bearer call never carries the human's cookies.
105
+
106
+ - Initial skeleton — `Kiosk::UserIdentityProviders::Devise` extending `Kiosk::UserIdentityProviders::Base`.
107
+ - `#verify(request)` reads the signed-in user from the request's Warden proxy (`request.env["warden"].user` — the shipped wire, where kiosk-server's `IdentityResolution.resolve` passes an `ActionDispatch::Request`) and returns a `Kiosk::Identity` with `actor: "human"`. Returns `nil` when no user is signed in — which covers unauthenticated, locked, and unconfirmed users uniformly since Devise's `active_for_authentication?` already gates the Warden user.
108
+ - Also accepts a controller-shaped object exposing `#current_user`, and a bare Rack `env` Hash carrying `env["warden"]`, for hosts that pass either directly.
109
+ - Role resolution: `user.kiosk_role` if defined, else first of `Kiosk.configuration.roles`. Raises `Kiosk::UserIdentityProviders::Devise::ConfigurationError` with a help-style message when both fall through.
110
+ - Honours `Kiosk.configuration.user_id_column` (default `:id`).
111
+ - Inherits the default `user_active?` from {Kiosk::UserIdentityProviders::Base} — embedded mode relies on Devise's per-request `active_for_authentication?` hook rather than an opt-in callback.
112
+
113
+ ### Design decisions
114
+
115
+ - **No hard `devise` runtime dependency.** The adapter only reads the request's Warden user (`request.env["warden"].user`); the provider's installed Devise satisfies the requirement. Documenting this in `README.md` is cleaner than forcing a Bundler constraint that doesn't match reality (kiosk-server's `IdentityResolution.resolve` passes an `ActionDispatch::Request` — Devise itself isn't a constructor argument).
116
+ - **No test-time `devise` dependency.** Tests use a stub `current_user`; loading Devise just to verify a one-line read would be ceremony.
117
+
118
+ ### Out of scope for first release
119
+
120
+ - `Kiosk::HumanContext` cross-channel session mixing.
121
+ - Full Rails controller integration test (needs a host app — lands with kiosk-server's request specs).
122
+ - `kiosk-devise` — the broader Devise integration with kiosk-server's controllers, generators, and approval-form partial.
data/LICENSE.txt ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # kiosk-user-idp-devise
2
+
3
+ The Devise user-IdP adapter for [Kiosk](https://kiosk.tech) — for Rails providers that authenticate principals through Devise. Install it explicitly; it is not bundled by `kiosk-all`.
4
+
5
+ ## What it does
6
+
7
+ Reads the signed-in user from the incoming request's Warden proxy and returns a `Kiosk::Identity` for the human-side surfaces: the device-verify page, the linked-assistants page, and `POST /kiosk/auth/link` and `/kiosk/auth/unlink`. The wire and the event stream never take it; they accept only the assistant's bearer token.
8
+
9
+ The adapter is **agnostic about how the user logged in**: Devise's `database_authenticatable` and `omniauthable` modules both populate the request's Warden user, so the same read covers password login, passwordless magic-link, and every OmniAuth strategy (Google, GitHub, SAML, …).
10
+
11
+ **Lockable / confirmable handling is implicit.** Devise's `active_for_authentication?` already gates the Warden user, so a locked or unconfirmed user yields no signed-in user and the request fails as unauthenticated. No extra code in the adapter.
12
+
13
+ ## Install
14
+
15
+ > **Not on RubyGems yet** — so every `gem` line below carries `github: "kiosk-hq/kiosk"`, which is what makes it copy-pasteable today. Publication status and the canonical install are stated once, in the monorepo README's [Install](https://github.com/kiosk-hq/kiosk#install) section.
16
+
17
+ Add the adapter explicitly — the `kiosk-all` meta-gem pulls in only `kiosk-core` and `kiosk-server`, so IdP adapters are opt-in per provider:
18
+
19
+ ```ruby
20
+ gem "kiosk-user-idp-devise", github: "kiosk-hq/kiosk"
21
+ ```
22
+
23
+ The adapter declares no hard runtime dependency on `devise` — it only reads the request's Warden user, and your provider's already-installed Devise satisfies the requirement.
24
+
25
+ ## Wire up
26
+
27
+ ```ruby
28
+ # config/initializers/kiosk.rb
29
+ require "kiosk/user_identity_providers/devise"
30
+
31
+ Kiosk.configure do |c|
32
+ c.user_idp = Kiosk::UserIdentityProviders::Devise.new
33
+ c.roles = %i[customer]
34
+ # Required whenever `c.roles` is set, and refused at boot without it: the
35
+ # default role an assistant lands on when nothing else resolves one. Declaring
36
+ # no roles at all is the other supported shape, and then neither line is set.
37
+ c.registration_role = :customer
38
+ end
39
+ ```
40
+
41
+ ## Pointing assistants at the wire
42
+
43
+ An assistant that POSTs JSON at the sign-in page, or signs out with no session, gets a JSON body naming `/.well-known/kiosk.json` (`422 invalid_authenticity_token`, `401 not_signed_in`) instead of an empty error. Browsers are answered as before.
44
+
45
+ ```ruby
46
+ class ApplicationController < ActionController::Base
47
+ include Kiosk::UserIdentityProviders::Devise::WireSignpost
48
+ end
49
+ ```
50
+
51
+ ```ruby
52
+ # config/routes.rb
53
+ devise_for :users, controllers: { sessions: "kiosk/user_identity_providers/devise/sessions" }
54
+ ```
55
+
56
+ ## Driving the sign-in from a script
57
+
58
+ An origin wired to this adapter has no stub user-IdP, so anything that needs the HUMAN half of a ceremony — approving an assistant on the device-verify page, minting a link code, unlinking — has to hold a real browser session. `DeviseSession` is that session, and it is the client end of the same contract the adapter above serves:
59
+
60
+ ```ruby
61
+ require "kiosk/user_identity_providers/devise_session"
62
+
63
+ session = Kiosk::UserIdentityProviders::DeviseSession.new(ENV.fetch("SERVER_URL"))
64
+ session.sign_in!(email: "alice@example.com", password: "…")
65
+
66
+ rc, link = session.post_json("/kiosk/auth/link", {}, { session: true })
67
+ ```
68
+
69
+ Nothing in it is a test double — it drives the shipped Devise routes over real HTTP exactly as a browser does. `session: true` is the only knob: it marks the calls that are the HUMAN's, so an agent's own Bearer calls never carry the human's cookie jar.
70
+
71
+ ## Role resolution
72
+
73
+ Each Kiosk token carries exactly one active role. For a Devise-authenticated human:
74
+
75
+ 1. If the user model defines `#kiosk_role`, that wins.
76
+ 2. Otherwise, the first symbol in `Kiosk.configuration.roles` is used.
77
+ 3. If `roles` is empty AND `#kiosk_role` is absent, `Kiosk::UserIdentityProviders::Devise::ConfigurationError` is raised.
78
+
79
+ ```ruby
80
+ class User < ApplicationRecord
81
+ devise :database_authenticatable
82
+
83
+ # Optional — only if you need per-user role choice.
84
+ def kiosk_role
85
+ support_staff? ? :customer_support : :customer
86
+ end
87
+ end
88
+ ```
89
+
90
+ ## Request shape
91
+
92
+ kiosk-server calls the adapter with an `ActionDispatch::Request` from the human-side page gate and from the link endpoints. The adapter reads the signed-in user from the Warden proxy of that request, `request.env["warden"].user`. Devise sets this proxy when its middleware has run. The adapter also accepts an object that has `#current_user`, such as a controller, and a bare Rack `env` Hash that has `env["warden"]`.
93
+
94
+ ## Status
95
+
96
+ Pre-v1.0 alpha. API surface stable across pre-v1.0 minor bumps.
97
+
98
+ ## License
99
+
100
+ Apache-2.0 — see `LICENSE.txt`.
101
+
102
+ ## Links
103
+
104
+ - [kiosk.tech](https://kiosk.tech)
105
+ - [Issue tracker](https://github.com/kiosk-hq/kiosk/issues)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kiosk
4
+ module UserIdentityProviders
5
+ class Devise
6
+ # The Devise adapter's setting, added to {Kiosk::Configuration}.
7
+ module ConfigurationExtension
8
+ # The user model's column that holds the customer's id. Default :id.
9
+ attr_writer :user_id_column
10
+
11
+ def user_id_column
12
+ @user_id_column ||= :id
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ Kiosk::Configuration.include(Kiosk::UserIdentityProviders::Devise::ConfigurationExtension)
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kiosk/user_identity_providers/devise/wire_signpost'
4
+
5
+ module Kiosk
6
+ module UserIdentityProviders
7
+ class Devise
8
+ # Devise's sign-in and sign-out, answering a JSON sign-out with no session
9
+ # to end with a pointer to the wire. Route it with
10
+ # `devise_for :users, controllers: { sessions: "kiosk/user_identity_providers/devise/sessions" }`.
11
+ class SessionsController < ::Devise::SessionsController
12
+ include WireSignpost
13
+
14
+ private
15
+
16
+ def respond_to_on_destroy(non_navigational_status: :no_content)
17
+ return super unless non_navigational_status == :unauthorized && kiosk_json_request?
18
+
19
+ render_wire_signpost :unauthorized, 'not_signed_in',
20
+ message: 'there is no human web session to end — /users/sign_out closes a ' \
21
+ 'browser session on this site, not a Kiosk credential',
22
+ hint: 'assistant credentials live on the wire and are dropped there: GET ' \
23
+ "#{request.base_url}/.well-known/kiosk.json for the register/login " \
24
+ 'and revoke endpoints'
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Standalone so the gemspec can read the version without loading kiosk-core.
4
+ module Kiosk
5
+ module UserIdentityProviders
6
+ module DeviseVersion
7
+ VERSION = '0.5.20'
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'kiosk/user_identity_providers/devise'
5
+
6
+ module Kiosk
7
+ module UserIdentityProviders
8
+ class Devise
9
+ # Answers a JSON caller at a human page with a pointer to the Kiosk wire
10
+ # instead of an empty error. Include it in ApplicationController.
11
+ module WireSignpost
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ rescue_from ActionController::InvalidAuthenticityToken do |error|
16
+ raise error unless kiosk_json_request?
17
+
18
+ render_wire_signpost :unprocessable_entity, 'invalid_authenticity_token',
19
+ message: 'this is the human sign-in page, not the Kiosk wire — it needs a ' \
20
+ 'browser session and a CSRF token from its own form',
21
+ hint: 'assistants authenticate with their own keypair: GET ' \
22
+ "#{request.base_url}/.well-known/kiosk.json for the register/login " \
23
+ 'endpoints, the catalog link and the modules this origin serves'
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def kiosk_json_request?
30
+ request.format.json? || !!request.content_mime_type&.json?
31
+ rescue StandardError
32
+ false
33
+ end
34
+
35
+ def render_wire_signpost(status, code, message:, hint:)
36
+ render status:, json: { ok: false, error: { code:, message:, hint: } }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kiosk'
4
+
5
+ module Kiosk
6
+ module UserIdentityProviders
7
+ # The user-IdP for a Rails origin whose humans sign in through Devise: it
8
+ # reads the request's Warden user, so password, magic-link and OmniAuth
9
+ # sign-ins all resolve, and a locked or unconfirmed user resolves to nil.
10
+ class Devise < Base
11
+ autoload :SessionsController, 'kiosk/user_identity_providers/devise/sessions_controller'
12
+ autoload :WireSignpost, 'kiosk/user_identity_providers/devise/wire_signpost'
13
+
14
+ # Raised when neither the user model nor the configuration names a role.
15
+ class ConfigurationError < StandardError; end
16
+
17
+ NO_ROLE = <<~MSG.strip
18
+ Cannot resolve a role for the Devise principal: \
19
+ `Kiosk.configuration.roles` is empty and the user model does not \
20
+ define `#kiosk_role`. Configure at least one role — e.g. \
21
+ `Kiosk.configure { |c| c.roles = %i[customer] }` — or add \
22
+ `def kiosk_role; :customer; end` to your user model.
23
+ MSG
24
+
25
+ # @param request [#env, #current_user, Hash] an ActionDispatch::Request,
26
+ # a controller exposing `#current_user`, or a Rack env Hash
27
+ # @return [Kiosk::Identity, nil] nil when no user is signed in
28
+ def verify(request)
29
+ user = current_user_from(request)
30
+ return nil if user.nil?
31
+
32
+ Kiosk::Identity.new(
33
+ user_id: user.public_send(Kiosk.configuration.user_id_column),
34
+ role: role_for(user),
35
+ actor: 'human',
36
+ agent_id: nil,
37
+ claims: {}
38
+ )
39
+ end
40
+
41
+ private
42
+
43
+ def current_user_from(request)
44
+ return request.current_user if request.respond_to?(:current_user)
45
+
46
+ env = rack_env_for(request)
47
+ return nil if env.nil?
48
+
49
+ warden = env['warden']
50
+ warden&.user
51
+ end
52
+
53
+ def rack_env_for(request)
54
+ return request.env if request.respond_to?(:env)
55
+ return request if request.is_a?(Hash)
56
+
57
+ nil
58
+ end
59
+
60
+ # The model's `#kiosk_role` verbatim, nil included; else the first configured role.
61
+ def role_for(user)
62
+ return user.kiosk_role if user.respond_to?(:kiosk_role)
63
+
64
+ roles = Kiosk.configuration.roles
65
+ raise ConfigurationError, NO_ROLE if Array(roles).empty?
66
+
67
+ roles.first
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ require 'kiosk/user_identity_providers/devise/version'
74
+ require 'kiosk/user_identity_providers/devise/configuration_extension'
75
+
76
+ Kiosk::UserIdentityProviders::Devise::VERSION =
77
+ Kiosk::UserIdentityProviders::DeviseVersion::VERSION
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+
7
+ module Kiosk
8
+ module UserIdentityProviders
9
+ # Signs a human in to a Devise origin through its real form and keeps the
10
+ # session cookie; `session: true` on a call sends that cookie, so an
11
+ # assistant's own Bearer calls never carry it.
12
+ class DeviseSession
13
+ # Raised when the sign-in handshake does not reach a signed-in session.
14
+ class SignInError < StandardError; end
15
+
16
+ attr_reader :server, :cookies
17
+
18
+ def initialize(server)
19
+ @server = server.to_s.sub(%r{/\z}, '')
20
+ @uri = URI(@server)
21
+ @cookies = {}
22
+ end
23
+
24
+ # Raises SignInError unless Devise answers the form with a redirect.
25
+ def sign_in!(email:, password:)
26
+ form = get_html('/users/sign_in')
27
+ raise SignInError, "sign-in form: #{form.code}" unless form.code.to_i == 200
28
+
29
+ res = post_form('/users/sign_in',
30
+ 'authenticity_token' => csrf_token(form.body),
31
+ 'user[email]' => email,
32
+ 'user[password]' => password)
33
+ raise SignInError, "sign-in failed: #{res.code}" unless [302, 303].include?(res.code.to_i)
34
+
35
+ self
36
+ end
37
+
38
+ # GET an HTML page over the session (cookies attached — this is the human).
39
+ def get_html(path)
40
+ req = Net::HTTP::Get.new(uri_for(path))
41
+ req['Cookie'] = cookie_header unless @cookies.empty?
42
+ request(req)
43
+ end
44
+
45
+ # POST a form over the session (cookies attached — this is the human).
46
+ def post_form(path, form, headers = {})
47
+ req = Net::HTTP::Post.new(uri_for(path), headers)
48
+ req['Cookie'] = cookie_header unless @cookies.empty?
49
+ req.set_form_data(form)
50
+ request(req)
51
+ end
52
+
53
+ # `session: true` in the headers sends the human's cookies.
54
+ def post_json(path, body, headers = {})
55
+ headers = headers.dup
56
+ session = headers.delete(:session)
57
+ req = Net::HTTP::Post.new(uri_for(path), { 'Content-Type' => 'application/json' }.merge(headers))
58
+ req['Cookie'] = cookie_header if session
59
+ req.body = JSON.generate(body)
60
+ parsed(request(req))
61
+ end
62
+
63
+ # `session: true` in the headers sends the human's cookies.
64
+ def get_json(path, params = {}, headers = {})
65
+ headers = headers.dup
66
+ session = headers.delete(:session)
67
+ uri = uri_for(path)
68
+ uri.query = URI.encode_www_form(params) unless params.empty?
69
+ req = Net::HTTP::Get.new(uri, headers)
70
+ req['Cookie'] = cookie_header if session
71
+ parsed(request(req))
72
+ end
73
+
74
+ # The CSRF token Rails embeds in a rendered form.
75
+ def csrf_token(html)
76
+ html[/name="authenticity_token" value="([^"]+)"/, 1]
77
+ end
78
+
79
+ def cookie_header = @cookies.map { |k, v| "#{k}=#{v}" }.join('; ')
80
+
81
+ # Sends a request built by the caller, absorbing every Set-Cookie; TLS follows the target's scheme.
82
+ def request(req)
83
+ target = req.uri || @uri
84
+ http = Net::HTTP.new(target.hostname, target.port)
85
+ http.use_ssl = target.scheme == 'https'
86
+ res = http.request(req)
87
+ Array(res.get_fields('set-cookie')).each do |line|
88
+ name, value = line.split(';').first.split('=', 2)
89
+ @cookies[name] = value
90
+ end
91
+ res
92
+ end
93
+
94
+ private
95
+
96
+ def uri_for(path)
97
+ path.to_s.start_with?('http') ? URI(path) : URI("#{@server}#{path}")
98
+ end
99
+
100
+ def parsed(res)
101
+ [res.code.to_i, begin
102
+ JSON.parse(res.body)
103
+ rescue StandardError
104
+ {}
105
+ end]
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ # rubocop:disable Naming/FileName -- Bundler.require loads the file named after the gem.
2
+ # frozen_string_literal: true
3
+
4
+ require 'kiosk/user_identity_providers/devise'
5
+ # rubocop:enable Naming/FileName
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kiosk-user-idp-devise
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.20
5
+ platform: ruby
6
+ authors:
7
+ - Phil Pirozhkov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: kiosk-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.5.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.5.0
26
+ description: |
27
+ kiosk-user-idp-devise is the opt-in user-IdP adapter for Rails
28
+ providers that authenticate principals through Devise. Add it to your
29
+ Gemfile explicitly — the kiosk-all meta-gem pulls in only kiosk-core
30
+ and kiosk-server, so IdP adapters are chosen per provider.
31
+
32
+ It reads the signed-in user from the request's Warden proxy
33
+ (`request.env["warden"].user`) passed by kiosk-server and returns a
34
+ {Kiosk::Identity} value object. Covers both Devise paths —
35
+ `database_authenticatable` and `omniauthable` — since both populate the
36
+ Warden user, so the adapter is agnostic to how the user logged in.
37
+
38
+ Lockable / confirmable handling comes for free: Devise's
39
+ `active_for_authentication?` already gates the Warden user, so a locked
40
+ or unconfirmed user yields no signed-in user and the request fails as
41
+ unauthenticated with no extra code.
42
+
43
+ No hard runtime dependency on Devise itself — the adapter only reads the
44
+ request's Warden user, so the provider's already-installed Devise
45
+ satisfies the requirement.
46
+ email:
47
+ - hello@fili.pp.ru
48
+ executables: []
49
+ extensions: []
50
+ extra_rdoc_files: []
51
+ files:
52
+ - CHANGELOG.md
53
+ - LICENSE.txt
54
+ - README.md
55
+ - lib/kiosk-user-idp-devise.rb
56
+ - lib/kiosk/user_identity_providers/devise.rb
57
+ - lib/kiosk/user_identity_providers/devise/configuration_extension.rb
58
+ - lib/kiosk/user_identity_providers/devise/sessions_controller.rb
59
+ - lib/kiosk/user_identity_providers/devise/version.rb
60
+ - lib/kiosk/user_identity_providers/devise/wire_signpost.rb
61
+ - lib/kiosk/user_identity_providers/devise_session.rb
62
+ homepage: https://kiosk.tech
63
+ licenses:
64
+ - Apache-2.0
65
+ metadata:
66
+ homepage_uri: https://kiosk.tech
67
+ source_code_uri: https://github.com/kiosk-hq/kiosk
68
+ changelog_uri: https://github.com/kiosk-hq/kiosk/blob/main/kiosk-user-idp-devise/CHANGELOG.md
69
+ bug_tracker_uri: https://github.com/kiosk-hq/kiosk/issues
70
+ rubygems_mfa_required: 'true'
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '4.0'
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubygems_version: 4.0.3
86
+ specification_version: 4
87
+ summary: Devise user-IdP adapter for the Kiosk framework
88
+ test_files: []