veri 0.3.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4fcb8a0d60277a1b18a9c23114d96c19472794ee47c02d62445248ff30f16f0
4
- data.tar.gz: fa8eb3f99daf00c2c7c338350f14d3014ab855923e6977581d60e376c9dc3222
3
+ metadata.gz: 85a6c779bd69de4d11430630c142f8e5e12e3574d7828f740abc19f0e51d291f
4
+ data.tar.gz: 0def50b534c6d728230ec7543fe6a007a518f9abe47e9c0d93aac45e7eb2f59f
5
5
  SHA512:
6
- metadata.gz: d21bba8f857717fd6e9c54b1444c23feb2f5dea73dccda0ca3d661155b6a79d2901b8f4a58717bdb28527d6e1576222688fd802da86c18e86055d301e6c2003f
7
- data.tar.gz: a3547cee5d2bf4190fc95f923177da126e4f478ef8dc052d49bbdc524515be211d05ab21a0636fd373edb9b7d24b76049e7ef40bf826d667e85226da0b4ab7b1
6
+ metadata.gz: 714985726d7e5d04f55dbb1b94a73110a64ed1cb2853f565ddefda540cb13393969a1c4c3ec91e7de310822224dd844031d7d6b0cf57bd3b537c36da5bb6d55b
7
+ data.tar.gz: 7f2070d1f1da0260a8061b8abcff5a625245e4388f593d14a6edbdc8c8b496a3b55b5525bfda7a9abaae6bd91ffb218cb0d707d888f14114981c536d99a85e90
data/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## v0.4.0
2
+
3
+ ### Breaking
4
+
5
+ - Changed `veri_sessions` table to support multi-tenancy
6
+ - Renamed `revert_to_true_identity` session method to `to_true_identity`
7
+ - Session method `prune` no longer accepts a user argument
8
+ - Session method `terminate_all` no longer accepts a user argument
9
+
10
+ ### Features
11
+
12
+ - Added multi-tenancy support
13
+ - Added session scopes to fetch active, expired, and inactive sessions
14
+ - Added user scopes to fetch locked and unlocked users
15
+
1
16
  ## v0.3.1
2
17
 
3
18
  ### Misc
data/README.md CHANGED
@@ -13,6 +13,7 @@ Veri is a cookie-based authentication library for Ruby on Rails that provides es
13
13
  - Return path handling
14
14
  - User impersonation feature
15
15
  - Account lockout functionality
16
+ - Multi-tenancy support
16
17
 
17
18
  > ⚠️ **Development Notice**<br>
18
19
  > Veri is functional but in early development. Breaking changes may occur in minor releases until v1.0!
@@ -26,6 +27,7 @@ Veri is a cookie-based authentication library for Ruby on Rails that provides es
26
27
  - [Controller Integration](#controller-integration)
27
28
  - [Authentication Sessions](#authentication-sessions)
28
29
  - [Account Lockout](#account-lockout)
30
+ - [Multi-Tenancy](#multi-tenancy)
29
31
  - [View Helpers](#view-helpers)
30
32
  - [Testing](#testing)
31
33
 
@@ -84,10 +86,10 @@ Your user model is automatically extended with password management methods:
84
86
 
85
87
  ```rb
86
88
  # Set or update a password
87
- user.update_password("new_password")
89
+ user.update_password("password")
88
90
 
89
91
  # Verify a password
90
- user.verify_password("submitted_password")
92
+ user.verify_password("password")
91
93
  ```
92
94
 
93
95
  ## Controller Integration
@@ -135,14 +137,27 @@ class SessionsController < ApplicationController
135
137
  end
136
138
  ```
137
139
 
138
- Available methods:
140
+ Available controller methods:
139
141
 
140
- - `current_user` - Returns authenticated user or `nil`
141
- - `logged_in?` - Returns `true` if user is authenticated
142
- - `log_in(user)` - Authenticates user and creates session, returns `true` on success or `false` if account is locked
143
- - `log_out` - Terminates current session
144
- - `return_path` - Returns path user was accessing before authentication
145
- - `current_session` - Returns current authentication session
142
+ ```rb
143
+ # Returns authenticated user or nil
144
+ current_user
145
+
146
+ # Returns true if user is authenticated
147
+ logged_in?
148
+
149
+ # Authenticates user and creates session, returns true on success or false if account is locked
150
+ log_in(user)
151
+
152
+ # Terminates current session
153
+ log_out
154
+
155
+ # Returns path user was trying to access before authentication, if any
156
+ return_path
157
+
158
+ # Returns current authentication session
159
+ current_session
160
+ ```
146
161
 
147
162
  ### User Impersonation (Shapeshifting)
148
163
 
@@ -159,7 +174,7 @@ module Admin
159
174
 
160
175
  def destroy
161
176
  original_user = current_session.true_identity
162
- current_session.revert_to_true_identity
177
+ current_session.to_true_identity
163
178
  redirect_to admin_dashboard_path, notice: "Returned to #{original_user.name}"
164
179
  end
165
180
  end
@@ -168,14 +183,26 @@ end
168
183
 
169
184
  Available session methods:
170
185
 
171
- - `shapeshift(user)` - Assume another user's identity (maintains original identity)
172
- - `revert_to_true_identity` - Return to original identity
173
- - `shapeshifted?` - Returns true if currently shapeshifted
174
- - `true_identity` - Returns original user when shapeshifted, otherwise current user
186
+ ```rb
187
+ # Assume another user's identity (maintains original identity)
188
+ session.shapeshift(user)
189
+
190
+ # Return to original identity
191
+ session.to_true_identity
192
+
193
+ # Returns true if currently shapeshifted
194
+ session.shapeshifted?
195
+
196
+ # Returns original user when shapeshifted, otherwise current user
197
+ session.true_identity
198
+ ```
175
199
 
176
200
  Controller helper:
177
201
 
178
- - `shapeshifter?` - Returns true if the current session is shapeshifted
202
+ ```rb
203
+ # Returns true if the current session is shapeshifted
204
+ shapeshifter?
205
+ ```
179
206
 
180
207
  ### When unauthenticated
181
208
 
@@ -208,7 +235,7 @@ Veri stores authentication sessions in the database, providing session managemen
208
235
 
209
236
  ```rb
210
237
  # Get all sessions for a user
211
- user.veri_sessions
238
+ user.sessions
212
239
 
213
240
  # Get current session in controller
214
241
  current_session
@@ -219,6 +246,7 @@ current_session
219
246
  ```rb
220
247
  session.identity
221
248
  # => authenticated user
249
+
222
250
  session.info
223
251
  # => {
224
252
  # device: "Desktop",
@@ -232,9 +260,23 @@ session.info
232
260
  ### Session Status
233
261
 
234
262
  ```rb
235
- session.active? # Session is active (neither expired nor inactive)
236
- session.inactive? # Session exceeded inactivity timeout
237
- session.expired? # Session exceeded maximum lifetime
263
+ # Session is active (neither expired nor inactive)
264
+ session.active?
265
+
266
+ # Session exceeded inactivity timeout
267
+ session.inactive?
268
+
269
+ # Session exceeded maximum lifetime
270
+ session.expired?
271
+
272
+ # Fetch active sessions
273
+ Veri::Session.active
274
+
275
+ # Fetch inactive sessions
276
+ Veri::Session.inactive
277
+
278
+ # Fetch expired sessions
279
+ Veri::Session.expired
238
280
  ```
239
281
 
240
282
  ### Session Management
@@ -243,12 +285,11 @@ session.expired? # Session exceeded maximum lifetime
243
285
  # Terminate a specific session
244
286
  session.terminate
245
287
 
246
- # Terminate all sessions for a user
247
- Veri::Session.terminate_all(user)
288
+ # Terminate all sessions
289
+ Veri::Session.terminate_all
248
290
 
249
291
  # Clean up expired/inactive sessions
250
- Veri::Session.prune # All sessions
251
- Veri::Session.prune(user) # Specific user's sessions
292
+ Veri::Session.prune
252
293
  ```
253
294
 
254
295
  ## Account Lockout
@@ -264,9 +305,62 @@ user.unlock!
264
305
 
265
306
  # Check if account is locked
266
307
  user.locked?
308
+
309
+ # Fetch locked users
310
+ User.locked
311
+
312
+ # Fetch unlocked users
313
+ User.unlocked
267
314
  ```
268
315
 
269
- When an account is locked, users cannot log in. If they're already logged in, their sessions will be terminated and they'll be treated as unauthenticated users.
316
+ When an account is locked, the user cannot log in. If the user is already logged in, their sessions will be terminated, and they will be treated as an unauthenticated user.
317
+
318
+ ## Multi-Tenancy
319
+
320
+ Veri supports multi-tenancy, allowing you to isolate authentication sessions between different tenants (e.g., organizations, clients, or subdomains).
321
+
322
+ ### Setting Up Multi-Tenancy
323
+
324
+ To enable multi-tenancy, override `current_tenant` method:
325
+
326
+ ```rb
327
+ class ApplicationController < ActionController::Base
328
+ include Veri::Authentication
329
+
330
+ with_authentication
331
+
332
+ private
333
+
334
+ def current_tenant
335
+ # Option 1: String-based tenancy (e.g., subdomain)
336
+ request.subdomain
337
+
338
+ # Option 2: Model-based tenancy (e.g., organization)
339
+ # Company.find_by(subdomain: request.subdomain)
340
+ end
341
+ end
342
+ ```
343
+
344
+ ### Session Tenant Access
345
+
346
+ Sessions expose their tenant through `tenant` method:
347
+
348
+ ```rb
349
+ # Returns the tenant (string, model instance, or nil in single-tenant applications)
350
+ session.tenant
351
+ ```
352
+
353
+ ### Migration Helpers
354
+
355
+ Handle tenant changes when models are renamed or removed. These are irreversible data migrations.
356
+
357
+ ```rb
358
+ # Rename a tenant class (e.g., when you rename your Organization model to Company)
359
+ migrate_authentication_tenant!("Organization", "Company")
360
+
361
+ # Remove orphaned tenant data (e.g., when you delete the Organization model entirely)
362
+ delete_authentication_tenant!("Organization")
363
+ ```
270
364
 
271
365
  ## View Helpers
272
366
 
@@ -10,6 +10,7 @@ class AddVeriAuthentication < ActiveRecord::Migration[<%= ActiveRecord::Migratio
10
10
  t.datetime :expires_at, null: false
11
11
  t.belongs_to :authenticatable, null: false, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }, index: true<%= ", type: :uuid" if options[:uuid] %>
12
12
  t.belongs_to :original_authenticatable, foreign_key: { to_table: <%= table_name.to_sym.inspect %> }, index: true<%= ", type: :uuid" if options[:uuid] %>
13
+ t.belongs_to :tenant, polymorphic: true, index: true<%= ", type: :uuid" if options[:uuid] %>
13
14
  t.datetime :shapeshifted_at
14
15
  t.datetime :last_seen_at, null: false
15
16
  t.string :ip_address
@@ -1,3 +1,5 @@
1
+ require "digest/sha2"
2
+
1
3
  module Veri
2
4
  module Authentication
3
5
  extend ActiveSupport::Concern
@@ -29,8 +31,8 @@ module Veri
29
31
  end
30
32
 
31
33
  def current_session
32
- token = cookies.encrypted[:veri_token]
33
- @current_session ||= token ? Session.find_by(hashed_token: Digest::SHA256.hexdigest(token)) : nil
34
+ token = cookies.encrypted["#{auth_cookie_prefix}_token"]
35
+ @current_session ||= token ? Session.find_by(hashed_token: Digest::SHA256.hexdigest(token), **resolved_tenant) : nil
34
36
  end
35
37
 
36
38
  def log_in(authenticatable)
@@ -41,14 +43,15 @@ module Veri
41
43
 
42
44
  return false if processed_authenticatable.locked?
43
45
 
44
- token = Veri::Session.establish(processed_authenticatable, request)
45
- cookies.encrypted.permanent[:veri_token] = { value: token, httponly: true }
46
+ token = Veri::Session.establish(processed_authenticatable, request, resolved_tenant)
47
+
48
+ cookies.encrypted.permanent["#{auth_cookie_prefix}_token"] = { value: token, httponly: true }
46
49
  true
47
50
  end
48
51
 
49
52
  def log_out
50
53
  current_session&.terminate
51
- cookies.delete(:veri_token)
54
+ cookies.delete("#{auth_cookie_prefix}_token")
52
55
  end
53
56
 
54
57
  def logged_in?
@@ -56,7 +59,7 @@ module Veri
56
59
  end
57
60
 
58
61
  def return_path
59
- cookies.signed[:veri_return_path]
62
+ cookies.signed["#{auth_cookie_prefix}_return_path"]
60
63
  end
61
64
 
62
65
  def shapeshifter?
@@ -79,7 +82,7 @@ module Veri
79
82
 
80
83
  log_out
81
84
 
82
- cookies.signed[:veri_return_path] = { value: request.fullpath, expires: 15.minutes.from_now } if request.get? && request.format.html?
85
+ cookies.signed["#{auth_cookie_prefix}_return_path"] = { value: request.fullpath, expires: 15.minutes.from_now } if request.get? && request.format.html?
83
86
 
84
87
  when_unauthenticated
85
88
  end
@@ -87,5 +90,19 @@ module Veri
87
90
  def when_unauthenticated
88
91
  request.format.html? ? redirect_back(fallback_location: root_path) : head(:unauthorized)
89
92
  end
93
+
94
+ def current_tenant = nil
95
+
96
+ def resolved_tenant
97
+ @resolved_tenant ||= Veri::Inputs::Tenant.new(
98
+ current_tenant,
99
+ error: Veri::InvalidTenantError,
100
+ message: "Expected a string, an ActiveRecord model instance, or nil, got `#{current_tenant.inspect}`"
101
+ ).resolve
102
+ end
103
+
104
+ def auth_cookie_prefix
105
+ @auth_cookie_prefix ||= "auth_#{Digest::SHA2.hexdigest(Marshal.dump(resolved_tenant))[0..7]}"
106
+ end
90
107
  end
91
108
  end
@@ -0,0 +1,20 @@
1
+ module Veri
2
+ module MigrationHelpers
3
+ def migrate_authentication_tenant!(old_tenant, new_tenant)
4
+ sessions = Veri::Session.where(tenant_type: old_tenant.to_s)
5
+
6
+ raise Veri::InvalidArgumentError, "No sessions exist in tenant #{old_tenant.inspect}" unless sessions.exists?
7
+ raise Veri::InvalidArgumentError, "Cannot migrate tenant to #{new_tenant.inspect}: class does not exist" unless new_tenant.to_s.safe_constantize
8
+
9
+ sessions.update_all(tenant_type: new_tenant.to_s)
10
+ end
11
+
12
+ def delete_authentication_tenant!(tenant)
13
+ sessions = Veri::Session.where(tenant_type: tenant.to_s)
14
+
15
+ raise Veri::InvalidArgumentError, "No sessions exist in tenant #{tenant.inspect}" unless sessions.exists?
16
+
17
+ sessions.delete_all
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ module Veri
2
+ module Inputs
3
+ class Tenant < Base
4
+ def resolve
5
+ case tenant = process
6
+ when nil
7
+ { tenant_type: nil, tenant_id: nil }
8
+ when String
9
+ { tenant_type: tenant.to_s, tenant_id: nil }
10
+ when ActiveRecord::Base
11
+ raise_error unless tenant.persisted?
12
+ { tenant_type: tenant.class.to_s, tenant_id: tenant.public_send(tenant.class.primary_key) }
13
+ else
14
+ tenant
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def type
21
+ -> {
22
+ self.class::Strict::String.constrained(min_size: 1) |
23
+ self.class::Instance(ActiveRecord::Base) |
24
+ self.class::Hash.schema(
25
+ tenant_type: self.class::Strict::String | self.class::Nil,
26
+ tenant_id: self.class::Strict::String | self.class::Strict::Integer | self.class::Nil
27
+ ) |
28
+ self.class::Nil
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -7,7 +7,10 @@ module Veri
7
7
 
8
8
  @@included = name
9
9
 
10
- has_many :veri_sessions, class_name: "Veri::Session", foreign_key: :authenticatable_id, dependent: :destroy
10
+ has_many :sessions, class_name: "Veri::Session", foreign_key: :authenticatable_id, dependent: :destroy
11
+
12
+ scope :locked, -> { where(locked: true) }
13
+ scope :unlocked, -> { where(locked: false) }
11
14
  end
12
15
 
13
16
  def update_password(password)
@@ -4,10 +4,16 @@ module Veri
4
4
  class Session < ActiveRecord::Base
5
5
  self.table_name = "veri_sessions"
6
6
 
7
- # rubocop:disable Rails/ReflectionClassName
8
7
  belongs_to :authenticatable, class_name: Veri::Configuration.user_model_name
9
8
  belongs_to :original_authenticatable, class_name: Veri::Configuration.user_model_name, optional: true
10
- # rubocop:enable Rails/ReflectionClassName
9
+ belongs_to :tenant, polymorphic: true, optional: true
10
+
11
+ scope :active, -> { where.not(id: expired.select(:id)).where.not(id: inactive.select(:id)) }
12
+ scope :expired, -> { where(expires_at: ...Time.current) }
13
+ scope :inactive, -> do
14
+ inactive_session_lifetime = Veri::Configuration.inactive_session_lifetime
15
+ inactive_session_lifetime ? where(last_seen_at: ...(Time.current - inactive_session_lifetime)) : none
16
+ end
11
17
 
12
18
  def active? = !expired? && !inactive?
13
19
 
@@ -60,7 +66,7 @@ module Veri
60
66
  )
61
67
  end
62
68
 
63
- def revert_to_true_identity
69
+ def to_true_identity
64
70
  update!(
65
71
  shapeshifted_at: nil,
66
72
  authenticatable: original_authenticatable,
@@ -68,49 +74,52 @@ module Veri
68
74
  )
69
75
  end
70
76
 
77
+ def tenant
78
+ return tenant_type if tenant_type.present? && tenant_id.blank?
79
+
80
+ record = super
81
+
82
+ raise ActiveRecord::RecordNotFound.new(nil, tenant_type, nil, tenant_id) if tenant_id.present? && !record
83
+
84
+ record
85
+ end
86
+
71
87
  class << self
72
- def establish(user, request)
88
+ def establish(user, request, resolved_tenant)
73
89
  token = SecureRandom.hex(32)
74
90
  expires_at = Time.current + Veri::Configuration.total_session_lifetime
75
91
 
76
92
  new(
77
93
  hashed_token: Digest::SHA256.hexdigest(token),
78
94
  expires_at:,
79
- authenticatable: Veri::Inputs::Authenticatable.new(user, error: Veri::Error).process
95
+ authenticatable: user,
96
+ **resolved_tenant
80
97
  ).update_info(request)
81
98
 
82
99
  token
83
100
  end
84
101
 
85
- def prune(user = nil)
86
- scope = if user
87
- where(
88
- authenticatable: Veri::Inputs::Authenticatable.new(
89
- user,
90
- optional: true,
91
- message: "Expected an instance of #{Veri::Configuration.user_model_name} or nil, got `#{user.inspect}`"
92
- ).process
93
- )
94
- else
95
- all
96
- end
97
-
98
- expired_scope = scope.where(expires_at: ...Time.current)
102
+ def prune
103
+ expired_scope = where(expires_at: ...Time.current)
99
104
 
100
105
  if Veri::Configuration.inactive_session_lifetime
101
106
  inactive_cutoff = Time.current - Veri::Configuration.inactive_session_lifetime
102
- expired_scope = expired_scope.or(scope.where(last_seen_at: ...inactive_cutoff))
107
+ expired_scope = expired_scope.or(where(last_seen_at: ...inactive_cutoff))
103
108
  end
104
109
 
105
110
  expired_scope.delete_all
106
- end
107
111
 
108
- def terminate_all(user)
109
- Veri::Inputs::Authenticatable.new(
110
- user,
111
- message: "Expected an instance of #{Veri::Configuration.user_model_name}, got `#{user.inspect}`"
112
- ).process.veri_sessions.delete_all
112
+ ids = where.not(tenant_id: nil).includes(:tenant).filter_map do |session|
113
+ session.tenant
114
+ nil
115
+ rescue ActiveRecord::RecordNotFound
116
+ session.id
117
+ end
118
+
119
+ where(id: ids).delete_all if ids.any?
113
120
  end
121
+
122
+ alias terminate_all delete_all
114
123
  end
115
124
  end
116
125
  end
data/lib/veri/railtie.rb CHANGED
@@ -4,9 +4,23 @@ module Veri
4
4
  class Railtie < Rails::Railtie
5
5
  initializer "veri.to_prepare" do |app|
6
6
  app.config.to_prepare do
7
+ if ActiveRecord::Base.connection.data_source_exists?("veri_sessions")
8
+ Veri::Session.where.not(tenant_id: nil).distinct.pluck(:tenant_type).each do |tenant_class|
9
+ tenant_class.constantize
10
+ rescue NameError => e
11
+ raise Veri::Error, "Tenant not found: class `#{e.name}` may have been renamed or deleted"
12
+ end
13
+ end
14
+
7
15
  user_model = Veri::Configuration.user_model
8
16
  user_model.include Veri::Authenticatable unless user_model < Veri::Authenticatable
9
17
  end
10
18
  end
19
+
20
+ initializer "veri.extend_migration_helpers" do
21
+ ActiveSupport.on_load :active_record do
22
+ ActiveRecord::Migration.include Veri::MigrationHelpers
23
+ end
24
+ end
11
25
  end
12
26
  end
data/lib/veri/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Veri
2
- VERSION = "0.3.1".freeze
2
+ VERSION = "0.4.0".freeze
3
3
  end
data/lib/veri.rb CHANGED
@@ -13,12 +13,14 @@ require_relative "veri/inputs/duration"
13
13
  require_relative "veri/inputs/hashing_algorithm"
14
14
  require_relative "veri/inputs/model"
15
15
  require_relative "veri/inputs/non_empty_string"
16
+ require_relative "veri/inputs/tenant"
16
17
  require_relative "veri/configuration"
17
18
 
18
19
  module Veri
19
20
  class Error < StandardError; end
20
21
  class InvalidArgumentError < Veri::Error; end
21
22
  class ConfigurationError < Veri::InvalidArgumentError; end
23
+ class InvalidTenantError < Veri::InvalidArgumentError; end
22
24
 
23
25
  delegate :configure, to: Veri::Configuration
24
26
  module_function :configure
@@ -27,5 +29,6 @@ end
27
29
  require_relative "veri/models/session"
28
30
  require_relative "veri/controllers/concerns/authentication"
29
31
  require_relative "veri/models/concerns/authenticatable"
32
+ require_relative "veri/helpers/migration_helpers"
30
33
 
31
34
  require_relative "veri/railtie"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: veri
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - enjaku4
@@ -125,12 +125,14 @@ files:
125
125
  - lib/veri.rb
126
126
  - lib/veri/configuration.rb
127
127
  - lib/veri/controllers/concerns/authentication.rb
128
+ - lib/veri/helpers/migration_helpers.rb
128
129
  - lib/veri/inputs/authenticatable.rb
129
130
  - lib/veri/inputs/base.rb
130
131
  - lib/veri/inputs/duration.rb
131
132
  - lib/veri/inputs/hashing_algorithm.rb
132
133
  - lib/veri/inputs/model.rb
133
134
  - lib/veri/inputs/non_empty_string.rb
135
+ - lib/veri/inputs/tenant.rb
134
136
  - lib/veri/models/concerns/authenticatable.rb
135
137
  - lib/veri/models/session.rb
136
138
  - lib/veri/password/argon2.rb
@@ -164,7 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
166
  - !ruby/object:Gem::Version
165
167
  version: '0'
166
168
  requirements: []
167
- rubygems_version: 3.7.1
169
+ rubygems_version: 3.6.7
168
170
  specification_version: 4
169
171
  summary: Minimal cookie-based authentication library for Ruby on Rails
170
172
  test_files: []