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 +7 -0
- data/README.md +365 -0
- data/clowk.gemspec +31 -0
- data/config/routes.rb +8 -0
- data/lib/clowk/authenticable.rb +112 -0
- data/lib/clowk/configuration.rb +46 -0
- data/lib/clowk/controllers/base_controller.rb +70 -0
- data/lib/clowk/controllers/callbacks_controller.rb +25 -0
- data/lib/clowk/controllers/sessions_controller.rb +23 -0
- data/lib/clowk/current.rb +47 -0
- data/lib/clowk/engine.rb +17 -0
- data/lib/clowk/helpers/url_helpers.rb +58 -0
- data/lib/clowk/http/client.rb +199 -0
- data/lib/clowk/http/logger_middleware.rb +29 -0
- data/lib/clowk/http/response.rb +43 -0
- data/lib/clowk/http/retry_middleware.rb +47 -0
- data/lib/clowk/http/timeout_middleware.rb +30 -0
- data/lib/clowk/jwt_verifier.rb +27 -0
- data/lib/clowk/middleware/token_extractor.rb +42 -0
- data/lib/clowk/sdk/client.rb +89 -0
- data/lib/clowk/sdk/resource.rb +68 -0
- data/lib/clowk/sdk/session.rb +11 -0
- data/lib/clowk/sdk/subdomain.rb +17 -0
- data/lib/clowk/sdk/token.rb +15 -0
- data/lib/clowk/sdk/user.rb +11 -0
- data/lib/clowk/subdomain.rb +120 -0
- data/lib/clowk/version.rb +5 -0
- data/lib/clowk.rb +56 -0
- metadata +136 -0
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
|