clowk 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 56e3503c335ec716e89e49428b66a0c4157be9cb6ff19c46095117062f4c7848
4
+ data.tar.gz: 1db10f568e025bc52ea97e255f9e4cdc8064507ace6adf83e74d78d20d5b5d9e
5
+ SHA512:
6
+ metadata.gz: 3ed1c6bb69c0644f5476374f8004ce865c1661aeec3729b42a59442ee453e6e431c23583d603ccf49b9e045ae2c0d7e9733f2482a040ef3418c069e6f88ef531
7
+ data.tar.gz: 393ed6db1f3777cc402731fe16e0874d64c842ab3b5118aa49c9a79e5b208f38a660b4bbcca45ee349eccf1f1be030ac35ff6e235310edbc9e14d57cd730a24b
data/README.md ADDED
@@ -0,0 +1,365 @@
1
+ # Clowk Ruby SDK
2
+
3
+ Clowk is the Ruby gem for integrating Clowk authentication into Rails applications.
4
+
5
+ It focuses on a small client-side surface:
6
+
7
+ - redirect users to Clowk
8
+ - verify the JWT returned by Clowk
9
+ - expose Rails-friendly auth helpers
10
+ - provide a minimal HTTP client for the Clowk API
11
+
12
+ ## Product domains
13
+
14
+ Clowk uses different domains for different concerns:
15
+
16
+ - `clowk.in`: public product site
17
+ - `app.clowk.in`: dashboard used to manage apps and instances
18
+ - `*.clowk.dev`: per-instance auth domain used by your end users
19
+
20
+ For a Rails app using this gem, the important part is the instance auth domain. Your app redirects users there, Clowk authenticates them, then redirects back with a signed JWT.
21
+
22
+ ## Install
23
+
24
+ ```ruby
25
+ # Gemfile
26
+ gem 'clowk'
27
+ ```
28
+
29
+ ## Quick start
30
+
31
+ ```ruby
32
+ # config/initializers/clowk.rb
33
+ Clowk.configure do |config|
34
+ config.publishable_key = ENV['CLOWK_PUBLISHABLE_KEY']
35
+ config.secret_key = ENV['CLOWK_SECRET_KEY']
36
+ end
37
+ ```
38
+
39
+ ```ruby
40
+ # config/routes.rb
41
+ Rails.application.routes.draw do
42
+ mount Clowk::Engine => '/clowk'
43
+ end
44
+ ```
45
+
46
+ ```ruby
47
+ class ApplicationController < ActionController::Base
48
+ include Clowk::Authenticable
49
+ end
50
+ ```
51
+
52
+ ```ruby
53
+ class DashboardController < ApplicationController
54
+ before_action :authenticate_clowk!
55
+
56
+ def index
57
+ @user = current_clowk
58
+ end
59
+ end
60
+ ```
61
+
62
+ ## Authentication flow
63
+
64
+ 1. Your app redirects the user to `*.clowk.dev`.
65
+ 2. Clowk authenticates the user.
66
+ 3. Clowk redirects back to your callback URL with `token` and `state`.
67
+ 4. The gem validates `state`, verifies the JWT, stores the authenticated session, and redirects back to a safe internal path.
68
+
69
+ The callback flow includes:
70
+
71
+ - session-backed `state` validation
72
+ - JWT verification with your instance `secret_key`
73
+ - internal-only redirect sanitization
74
+ - session reset before persisting the authenticated user
75
+ - `httponly` cookie persistence with `SameSite=Lax`
76
+
77
+ ## Configuration
78
+
79
+ ```ruby
80
+ Clowk.configure do |config|
81
+ config.secret_key = ENV['CLOWK_SECRET_KEY']
82
+ config.publishable_key = ENV['CLOWK_PUBLISHABLE_KEY']
83
+ config.subdomain_url = 'https://acme.clowk.dev'
84
+ config.prefix_by = :clowk
85
+
86
+ config.after_sign_in_path = '/'
87
+ config.after_sign_out_path = '/'
88
+
89
+ config.api_base_url = 'https://api.clowk.dev/client/v1'
90
+ config.callback_path = '/clowk/oauth/callback'
91
+ config.mount_path = '/clowk'
92
+
93
+ config.http_open_timeout = 5
94
+ config.http_read_timeout = 10
95
+ config.http_write_timeout = 10
96
+ config.http_retry_attempts = 2
97
+ config.http_retry_interval = 0.05
98
+ config.http_logger = Rails.logger
99
+ end
100
+ ```
101
+
102
+ Important settings:
103
+
104
+ | Setting | Purpose |
105
+ | --- | --- |
106
+ | `secret_key` | Required. Used to verify JWT signatures. |
107
+ | `publishable_key` | Preferred for auth URL resolution. The gem resolves the latest instance URL from it before sign in/sign up. |
108
+ | `subdomain_url` | Fallback auth domain when you do not want publishable-key-based resolution. |
109
+ | `prefix_by` | Prefix used to generate helper names. Default: `:clowk`. |
110
+ | `mount_path` | Local mount prefix used by helper path generation. Default: `/clowk`. |
111
+ | `callback_path` | Callback route Clowk redirects back to. Default: `/clowk/oauth/callback`. |
112
+ | `http_logger` | Optional logger used by `Clowk::Http`. |
113
+
114
+ Auth URL resolution priority:
115
+
116
+ 1. `publishable_key`
117
+ 2. `subdomain_url`
118
+
119
+ When `publishable_key` is present, the gem resolves the current auth base URL first and caches it briefly in memory. The lookup endpoint returns the full instance JSON payload, and the gem derives the final auth URL from that data (including `subdomain`). This keeps dashboard subdomain changes visible without redeploying the client app. If you do not want that lookup, configure only `subdomain_url`.
120
+
121
+ Internally, that lookup is done through `Clowk::SDK::Client`, via `client.subdomains.find_by_pk('pk_...')`.
122
+
123
+ If you mount the engine under a different prefix, keep `mount_path` and `callback_path` aligned with that choice.
124
+
125
+ Example:
126
+
127
+ ```ruby
128
+ Clowk.configure do |config|
129
+ config.mount_path = '/auth'
130
+ config.callback_path = '/auth/oauth/callback'
131
+ end
132
+
133
+ Rails.application.routes.draw do
134
+ mount Clowk::Engine => '/auth'
135
+ end
136
+ ```
137
+
138
+ ## Generated helpers
139
+
140
+ With the default `prefix_by = :clowk`, the concern exposes:
141
+
142
+ - `current_clowk`
143
+ - `authenticate_clowk!`
144
+ - `clowk_signed_in?`
145
+
146
+ You can change the prefix to avoid collisions with another auth system.
147
+
148
+ ```ruby
149
+ Clowk.configure do |config|
150
+ config.prefix_by = :member
151
+ end
152
+ ```
153
+
154
+ That generates:
155
+
156
+ - `current_member`
157
+ - `authenticate_member!`
158
+ - `member_signed_in?`
159
+
160
+ ## Current subject
161
+
162
+ `current_clowk` returns a `Clowk::Current` object.
163
+
164
+ ```ruby
165
+ current_clowk.id
166
+ current_clowk.email
167
+ current_clowk.name
168
+ current_clowk.avatar_url
169
+ current_clowk.provider
170
+ current_clowk.instance_id
171
+ current_clowk.app_id
172
+ ```
173
+
174
+ You can also access raw claims:
175
+
176
+ ```ruby
177
+ current_clowk[:sub]
178
+ current_clowk.to_h
179
+ ```
180
+
181
+ ## Route protection
182
+
183
+ ```ruby
184
+ class AdminController < ApplicationController
185
+ before_action :authenticate_clowk!
186
+ end
187
+ ```
188
+
189
+ Use `authenticate_clowk!` for protected pages. Use `current_clowk` when the route can be public but should still know who is authenticated.
190
+
191
+ ## View and URL helpers
192
+
193
+ Local mounted routes:
194
+
195
+ ```erb
196
+ <%= link_to 'Sign in', clowk_sign_in_path(return_to: dashboard_path) %>
197
+ <%= link_to 'Sign up', clowk_sign_up_path(return_to: dashboard_path) %>
198
+ <%= link_to 'Sign out', clowk_sign_out_path %>
199
+ ```
200
+
201
+ Direct remote URLs:
202
+
203
+ ```erb
204
+ <%= link_to 'Direct sign in', clowk_sign_in_url(redirect_to: dashboard_url) %>
205
+ <%= link_to 'Direct sign up', clowk_sign_up_url(redirect_to: dashboard_url) %>
206
+ ```
207
+
208
+ When `publishable_key` is configured, these helpers resolve the latest instance URL before building the final `sign-in` or `sign-up` destination. When it is absent, they use `subdomain_url` directly.
209
+
210
+ Mounted routes exposed by the engine:
211
+
212
+ - `/clowk/sign_in`
213
+ - `/clowk/sign_up`
214
+ - `/clowk/sign_out`
215
+ - `/clowk/oauth/callback`
216
+
217
+ When you mount the engine elsewhere, the same route set is exposed under your chosen prefix.
218
+
219
+ ## Token sources
220
+
221
+ The concern can read the token from:
222
+
223
+ - `params[:token]`
224
+ - cookies
225
+ - `Authorization: Bearer <token>`
226
+
227
+ That keeps the integration usable for callback routes, regular controllers, and API-style endpoints.
228
+
229
+ ## SDK Client
230
+
231
+ The gem includes a resource-oriented SDK for the Clowk API.
232
+
233
+ ```ruby
234
+ client = Clowk::SDK::Client.new
235
+ ```
236
+
237
+ The client uses your global configuration by default. You can also pass options explicitly:
238
+
239
+ ```ruby
240
+ client = Clowk::SDK::Client.new(
241
+ secret_key: 'sk_live_xxx',
242
+ publishable_key: 'pk_live_xxx'
243
+ )
244
+ ```
245
+
246
+ ### Resources
247
+
248
+ Each resource is accessible as a method on the client:
249
+
250
+ ```ruby
251
+ client.users
252
+ client.sessions
253
+ client.subdomains
254
+ ```
255
+
256
+ These return `Clowk::SDK::Resource` subclasses with a standard CRUD interface:
257
+
258
+ ```ruby
259
+ client.users.list
260
+ client.users.find('user_123')
261
+ client.users.show('user_123')
262
+ client.users.destroy('user_123')
263
+ ```
264
+
265
+ ### Token verification
266
+
267
+ ```ruby
268
+ response = client.tokens.verify(token: params[:token])
269
+
270
+ response.status # => 200
271
+ response.success? # => true
272
+ response.body_parsed # => { 'valid' => true }
273
+ ```
274
+
275
+ ### Subdomain resolution
276
+
277
+ ```ruby
278
+ response = client.subdomains.find_by_pk('pk_live_xxx')
279
+ ```
280
+
281
+ ### Search operators
282
+
283
+ The `search` method uses a Zendesk-style query syntax. You can use keyword arguments or a raw string for advanced operators.
284
+
285
+ **Keywords:**
286
+
287
+ ```ruby
288
+ client.users.search(email: "user@example.com")
289
+ # GET /users/search?query=email%3Auser%40example.com
290
+
291
+ client.users.search(status: "active", role: "admin")
292
+ # GET /users/search?query=status%3Aactive+role%3Aadmin
293
+ ```
294
+
295
+ **Raw string** for custom operators like `>`, `<`, `>=`:
296
+
297
+ ```ruby
298
+ client.users.search("email:user@example.com active:true created_at>2026-01-01")
299
+ # GET /users/search?query=email%3Auser%40example.com+active%3Atrue+created_at%3E2026-01-01
300
+ ```
301
+
302
+ The raw string is sent as-is, giving full control over the query syntax. The API backend parses and applies the operators.
303
+
304
+ ### Raw HTTP methods
305
+
306
+ The client also exposes raw HTTP methods for custom requests:
307
+
308
+ ```ruby
309
+ client.get('custom/endpoint')
310
+ client.post('custom/endpoint', { key: 'value' })
311
+ client.put('custom/endpoint', { key: 'value' })
312
+ client.patch('custom/endpoint', { key: 'value' })
313
+ client.delete('custom/endpoint')
314
+ client.head('custom/endpoint')
315
+ client.options('custom/endpoint')
316
+ ```
317
+
318
+ ## `Clowk::Http::Response`
319
+
320
+ HTTP responses are returned as `Clowk::Http::Response` objects.
321
+
322
+ ```ruby
323
+ response = client.get('users/user_123')
324
+
325
+ response.status # => 200
326
+ response.success? # => true
327
+ response.body # => '{"id":"user_123"}'
328
+ response.body_parsed # => { 'id' => 'user_123' }
329
+ response.headers # => { 'content-type' => ['application/json'] }
330
+ ```
331
+
332
+ For compatibility, the response also supports:
333
+
334
+ ```ruby
335
+ response[:status]
336
+ response[:success?]
337
+ response.to_h
338
+ ```
339
+
340
+ ## HTTP middleware
341
+
342
+ `Clowk::Http` uses a small internal middleware stack built on `Net::HTTP`.
343
+
344
+ Included behavior:
345
+
346
+ - request and response logging
347
+ - open, read, and write timeouts
348
+ - retry on retryable network errors
349
+ - automatic JSON request encoding
350
+ - automatic `body_parsed` JSON decoding when possible
351
+
352
+ ## Scope
353
+
354
+ This gem is intentionally narrow. It does not try to replace your entire app session architecture or act as an auth server.
355
+
356
+ Its job is to make the Rails side of Clowk integration predictable:
357
+
358
+ - start authentication
359
+ - validate callbacks safely
360
+ - expose a clean authenticated subject
361
+ - make future Clowk API access straightforward
362
+
363
+ ## License
364
+
365
+ MIT. See `LICENSE`.
data/clowk.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/clowk/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'clowk'
7
+ spec.version = Clowk::VERSION
8
+ spec.authors = ['Clowk']
9
+ spec.email = ['support@clowk.in']
10
+
11
+ spec.summary = 'Rails SDK for Clowk authentication'
12
+ spec.description = 'Clowk Authentication, JWT verification, and future API access'
13
+ spec.homepage = 'https://clowk.in'
14
+ spec.license = 'AGPL-3.0'
15
+ spec.required_ruby_version = '>= 3.3'
16
+ spec.metadata = {
17
+ 'rubygems_mfa_required' => 'true'
18
+ }
19
+
20
+ spec.files = Dir.chdir(__dir__) do
21
+ Dir['README.md', 'clowk.gemspec', 'config/routes.rb', 'lib/**/*.rb']
22
+ end
23
+
24
+ spec.require_paths = ['lib']
25
+
26
+ spec.add_dependency 'activesupport', '>= 7.0'
27
+ spec.add_dependency 'jwt', '>= 2.7', '< 3.0'
28
+ spec.add_dependency 'railties', '>= 7.0'
29
+
30
+ spec.add_development_dependency 'rspec', '>= 3.13', '< 4.0'
31
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ Clowk::Engine.routes.draw do
4
+ get '/sign_in', to: 'sessions#new', as: :sign_in
5
+ get '/sign_up', to: 'sessions#sign_up', as: :sign_up
6
+ match '/sign_out', to: 'sessions#destroy', via: %i(get delete), as: :sign_out
7
+ get '/oauth/callback', to: 'callbacks#show', as: :auth_callback
8
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Clowk
6
+ module Authenticable
7
+ extend ActiveSupport::Concern
8
+
9
+ def self.install_dynamic_methods(base)
10
+ scope = Clowk.config.prefix_by.to_s
11
+ current_method = :"current_#{scope}"
12
+ authenticate_method = :"authenticate_#{scope}!"
13
+ signed_in_method = :"#{scope}_signed_in?"
14
+
15
+ base.class_eval do
16
+ define_method(current_method) do
17
+ clowk_current_resource
18
+ end
19
+
20
+ define_method(authenticate_method) do
21
+ clowk_authenticate!
22
+ end
23
+
24
+ define_method(signed_in_method) do
25
+ clowk_current_resource.present?
26
+ end
27
+
28
+ helper_method current_method, authenticate_method, signed_in_method, :current_token if respond_to?(:helper_method)
29
+ end
30
+ end
31
+
32
+ included do
33
+ Clowk::Authenticable.install_dynamic_methods(self)
34
+ end
35
+
36
+ def clowk_current_resource
37
+ @clowk_current_resource ||= begin
38
+ payload = stored_user_payload || verified_request_payload
39
+ payload ? Current.new(payload) : nil
40
+ end
41
+ end
42
+
43
+ def current_token
44
+ stored_session&.dig('token') || extracted_token
45
+ end
46
+
47
+ def clowk_signed_in?
48
+ clowk_current_resource.present?
49
+ end
50
+
51
+ def clowk_authenticate!
52
+ return clowk_current_resource if clowk_signed_in?
53
+
54
+ if request.format.json?
55
+ render json: { error: 'Unauthorized' }, status: :unauthorized
56
+ else
57
+ redirect_to clowk_sign_in_path(return_to: request.fullpath)
58
+ end
59
+ end
60
+
61
+ def clowk_sign_out!
62
+ session.delete(Clowk.config.session_key)
63
+ cookies.delete(Clowk.config.cookie_key)
64
+
65
+ @clowk_current_resource = nil
66
+ end
67
+
68
+ private
69
+
70
+ def verified_request_payload
71
+ return unless extracted_token
72
+
73
+ payload = Clowk::JwtVerifier.new.verify(extracted_token)
74
+ persist_clowk_session(extracted_token, payload)
75
+
76
+ payload
77
+ rescue Clowk::InvalidTokenError
78
+ nil
79
+ end
80
+
81
+ def extracted_token
82
+ @extracted_token ||= Clowk::Middleware::TokenExtractor.new(request).call
83
+ end
84
+
85
+ def stored_session
86
+ raw_session = session[Clowk.config.session_key]
87
+ return unless raw_session.respond_to?(:to_h)
88
+
89
+ raw_session.to_h
90
+ end
91
+
92
+ def stored_user_payload
93
+ payload = stored_session&.dig('user') || stored_session&.dig(:user)
94
+ payload&.deep_symbolize_keys
95
+ end
96
+
97
+ def persist_clowk_session(token, payload)
98
+ session[Clowk.config.session_key] = {
99
+ token: token,
100
+ user: payload,
101
+ signed_in_at: Time.now.to_i
102
+ }
103
+
104
+ cookies[Clowk.config.cookie_key] = {
105
+ value: token,
106
+ httponly: true,
107
+ same_site: :lax,
108
+ secure: request.ssl?
109
+ }
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowk
4
+ class Configuration
5
+ attr_accessor :api_base_url
6
+ attr_accessor :app_base_url
7
+ attr_accessor :after_sign_in_path
8
+ attr_accessor :after_sign_out_path
9
+ attr_accessor :callback_path
10
+ attr_accessor :cookie_key
11
+ attr_accessor :http_logger
12
+ attr_accessor :http_open_timeout
13
+ attr_accessor :http_read_timeout
14
+ attr_accessor :http_retry_attempts
15
+ attr_accessor :http_retry_interval
16
+ attr_accessor :http_write_timeout
17
+ attr_accessor :issuer
18
+ attr_accessor :mount_path
19
+ attr_accessor :publishable_key
20
+ attr_accessor :prefix_by
21
+ attr_accessor :secret_key
22
+ attr_accessor :session_key
23
+ attr_accessor :subdomain_url
24
+ attr_accessor :token_param
25
+
26
+ def initialize
27
+ @api_base_url = 'https://api.clowk.dev/client/v1'
28
+ @app_base_url = 'https://app.clowk.in'
29
+ @after_sign_in_path = '/'
30
+ @after_sign_out_path = '/'
31
+ @mount_path = '/clowk'
32
+ @callback_path = '/clowk/oauth/callback'
33
+ @cookie_key = 'clowk_token'
34
+ @http_logger = nil
35
+ @http_open_timeout = 5
36
+ @http_read_timeout = 10
37
+ @http_retry_attempts = 2
38
+ @http_retry_interval = 0.05
39
+ @http_write_timeout = 10
40
+ @issuer = 'clowk'
41
+ @session_key = :clowk
42
+ @prefix_by = :clowk
43
+ @token_param = :token
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'uri'
5
+
6
+ module Clowk
7
+ class BaseController < ActionController::Base
8
+ include Clowk::Authenticable
9
+ include Clowk::Helpers::UrlHelpers
10
+
11
+ protect_from_forgery with: :exception
12
+
13
+ private
14
+
15
+ def redirect_back_or(default, return_to: params[:return_to])
16
+ redirect_target = safe_redirect_path(return_to) || safe_redirect_path(default) || '/'
17
+
18
+ redirect_to redirect_target
19
+ end
20
+
21
+ def start_clowk_auth_flow!(return_to: nil)
22
+ sanitized_return_to = safe_redirect_path(return_to) || safe_redirect_path(Clowk.config.after_sign_in_path) || '/'
23
+ state = SecureRandom.hex(32)
24
+
25
+ session[:clowk_auth_flow] = {
26
+ 'state' => state,
27
+ 'return_to' => sanitized_return_to
28
+ }
29
+
30
+ state
31
+ end
32
+
33
+ def consume_clowk_auth_flow!
34
+ flow = session.delete(:clowk_auth_flow)
35
+ return {} unless flow.respond_to?(:to_h)
36
+
37
+ flow.to_h
38
+ end
39
+
40
+ def validate_clowk_state!(expected_state, actual_state)
41
+ raise Clowk::InvalidStateError, 'missing state' if actual_state.blank?
42
+ raise Clowk::InvalidStateError, 'missing state' if expected_state.blank?
43
+ raise Clowk::InvalidStateError, 'invalid state' unless state_matches?(expected_state, actual_state)
44
+ end
45
+
46
+ def state_matches?(expected_state, actual_state)
47
+ return false if expected_state.bytesize != actual_state.bytesize
48
+
49
+ ActiveSupport::SecurityUtils.secure_compare(expected_state, actual_state)
50
+ end
51
+
52
+ def safe_redirect_path(candidate)
53
+ value = candidate.to_s
54
+ return if value.empty?
55
+
56
+ return value if value.start_with?('/') && !value.start_with?('//')
57
+
58
+ uri = URI.parse(value)
59
+ return unless uri.host == request.host && uri.scheme == request.scheme
60
+
61
+ uri.request_uri
62
+ rescue URI::InvalidURIError
63
+ nil
64
+ end
65
+
66
+ def reset_clowk_session!
67
+ reset_session
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowk
4
+ class CallbacksController < BaseController
5
+ def show
6
+ flow = consume_clowk_auth_flow!
7
+ validate_clowk_state!(flow['state'], params[:state])
8
+
9
+ token = params[Clowk.config.token_param]
10
+ raise Clowk::InvalidTokenError, 'missing token' if token.blank?
11
+
12
+ payload = Clowk::JwtVerifier.new.verify(token)
13
+ return_to = flow['return_to']
14
+
15
+ reset_clowk_session!
16
+ persist_clowk_session(token, payload)
17
+
18
+ redirect_back_or(Clowk.config.after_sign_in_path, return_to:)
19
+ rescue Clowk::InvalidTokenError, Clowk::InvalidStateError => e
20
+ flash[:alert] = "Clowk authentication failed: #{e.message}"
21
+
22
+ redirect_back_or(Clowk.config.after_sign_out_path, return_to: nil)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowk
4
+ class SessionsController < BaseController
5
+ def new
6
+ state = start_clowk_auth_flow!(return_to: params[:return_to])
7
+
8
+ redirect_to clowk_sign_in_url(state:), allow_other_host: true
9
+ end
10
+
11
+ def sign_up
12
+ state = start_clowk_auth_flow!(return_to: params[:return_to])
13
+
14
+ redirect_to clowk_sign_up_url(state:), allow_other_host: true
15
+ end
16
+
17
+ def destroy
18
+ clowk_sign_out!
19
+
20
+ redirect_back_or(Clowk.config.after_sign_out_path)
21
+ end
22
+ end
23
+ end