custos 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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +36 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +117 -0
  5. data/lib/custos/authenticatable.rb +30 -0
  6. data/lib/custos/callback_registry.rb +37 -0
  7. data/lib/custos/configuration.rb +35 -0
  8. data/lib/custos/controller_helpers.rb +47 -0
  9. data/lib/custos/mfa_encryptor.rb +63 -0
  10. data/lib/custos/model_config.rb +36 -0
  11. data/lib/custos/models/api_token.rb +23 -0
  12. data/lib/custos/models/magic_link_token.rb +13 -0
  13. data/lib/custos/models/mfa_credential.rb +22 -0
  14. data/lib/custos/models/remember_token.rb +11 -0
  15. data/lib/custos/plugin.rb +30 -0
  16. data/lib/custos/plugins/api_tokens.rb +48 -0
  17. data/lib/custos/plugins/email_confirmation.rb +59 -0
  18. data/lib/custos/plugins/lockout.rb +116 -0
  19. data/lib/custos/plugins/magic_link.rb +60 -0
  20. data/lib/custos/plugins/mfa.rb +185 -0
  21. data/lib/custos/plugins/password.rb +98 -0
  22. data/lib/custos/plugins/remember_me.rb +56 -0
  23. data/lib/custos/railtie.rb +15 -0
  24. data/lib/custos/session.rb +16 -0
  25. data/lib/custos/session_manager.rb +51 -0
  26. data/lib/custos/tasks/cleanup.rake +28 -0
  27. data/lib/custos/token_generator.rb +37 -0
  28. data/lib/custos/version.rb +5 -0
  29. data/lib/custos.rb +57 -0
  30. data/lib/generators/custos/install/install_generator.rb +23 -0
  31. data/lib/generators/custos/install/templates/create_custos_sessions.rb.tt +19 -0
  32. data/lib/generators/custos/install/templates/custos_initializer.rb.tt +12 -0
  33. data/lib/generators/custos/model/model_generator.rb +89 -0
  34. data/lib/generators/custos/model/templates/add_custos_columns.rb.tt +18 -0
  35. data/lib/generators/custos/model/templates/create_custos_api_tokens.rb.tt +18 -0
  36. data/lib/generators/custos/model/templates/create_custos_magic_links.rb.tt +17 -0
  37. data/lib/generators/custos/model/templates/create_custos_mfa_credentials.rb.tt +16 -0
  38. data/lib/generators/custos/model/templates/create_custos_remember_tokens.rb.tt +16 -0
  39. metadata +129 -0
data/lib/custos.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_record'
5
+
6
+ require 'custos/version'
7
+ require 'custos/configuration'
8
+ require 'custos/plugin'
9
+ require 'custos/callback_registry'
10
+ require 'custos/token_generator'
11
+ require 'custos/authenticatable'
12
+ require 'custos/model_config'
13
+ require 'custos/session'
14
+ require 'custos/session_manager'
15
+ require 'custos/controller_helpers'
16
+ require 'custos/mfa_encryptor'
17
+
18
+ require 'custos/models/magic_link_token'
19
+ require 'custos/models/api_token'
20
+ require 'custos/models/mfa_credential'
21
+ require 'custos/models/remember_token'
22
+
23
+ require 'custos/plugins/password'
24
+ require 'custos/plugins/magic_link'
25
+ require 'custos/plugins/api_tokens'
26
+ require 'custos/plugins/mfa'
27
+ require 'custos/plugins/lockout'
28
+ require 'custos/plugins/email_confirmation'
29
+ require 'custos/plugins/remember_me'
30
+
31
+ require 'custos/railtie' if defined?(Rails::Railtie)
32
+
33
+ module Custos
34
+ class Error < StandardError; end
35
+ class UnknownPluginError < Error; end
36
+ class NotAuthenticatedError < Error; end
37
+ class DecryptionError < Error; end
38
+ class TokenExpiredError < Error; end
39
+ class TokenInvalidError < Error; end
40
+ class AccountLockedError < Error; end
41
+
42
+ @mutex = Mutex.new
43
+
44
+ class << self
45
+ def configure
46
+ yield configuration
47
+ end
48
+
49
+ def configuration
50
+ @mutex.synchronize { @configuration ||= Configuration.new }
51
+ end
52
+
53
+ def reset_configuration!
54
+ @mutex.synchronize { @configuration = Configuration.new }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module Custos
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include ActiveRecord::Generators::Migration
10
+
11
+ source_root File.expand_path('templates', __dir__)
12
+
13
+ def create_initializer
14
+ template 'custos_initializer.rb.tt', 'config/initializers/custos.rb'
15
+ end
16
+
17
+ def create_sessions_migration
18
+ migration_template 'create_custos_sessions.rb.tt',
19
+ 'db/migrate/create_custos_sessions.rb'
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ class CreateCustosSessions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :custos_sessions do |t|
4
+ t.string :authenticatable_type, null: false
5
+ t.bigint :authenticatable_id, null: false
6
+ t.string :session_token_digest, null: false
7
+ t.string :ip_address
8
+ t.string :user_agent
9
+ t.datetime :last_active_at
10
+ t.datetime :revoked_at
11
+
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :custos_sessions, :session_token_digest, unique: true
16
+ add_index :custos_sessions, %i[authenticatable_type authenticatable_id],
17
+ name: "index_custos_sessions_on_authenticatable"
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ Custos.configure do |config|
4
+ # Session expiry in seconds (default: 24 hours)
5
+ # config.session_expiry = 24 * 60 * 60
6
+
7
+ # Session renewal interval in seconds (default: 1 hour)
8
+ # config.session_renewal_interval = 60 * 60
9
+
10
+ # Token length in bytes (default: 32)
11
+ # config.token_length = 32
12
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module Custos
7
+ module Generators
8
+ class ModelGenerator < Rails::Generators::Base
9
+ include ActiveRecord::Generators::Migration
10
+
11
+ KNOWN_PLUGINS = %w[password magic_link api_tokens mfa lockout email_confirmation remember_me].freeze
12
+
13
+ source_root File.expand_path('templates', __dir__)
14
+
15
+ argument :model_name, type: :string, desc: 'Model name (e.g. User)'
16
+ argument :plugins, type: :array, default: [], banner: 'plugin1 plugin2'
17
+
18
+ def validate_plugins
19
+ unknown = plugins.map(&:to_s) - KNOWN_PLUGINS
20
+ return if unknown.empty?
21
+
22
+ raise Thor::Error, "Unknown plugin(s): #{unknown.join(', ')}. " \
23
+ "Available: #{KNOWN_PLUGINS.join(', ')}"
24
+ end
25
+
26
+ def create_column_migration
27
+ columns = column_plugins & plugins.map(&:to_s)
28
+ return if columns.empty?
29
+
30
+ migration_template 'add_custos_columns.rb.tt',
31
+ "db/migrate/add_custos_to_#{table_name}.rb"
32
+ end
33
+
34
+ def create_magic_links_migration
35
+ return unless plugins.include?('magic_link')
36
+
37
+ migration_template 'create_custos_magic_links.rb.tt',
38
+ 'db/migrate/create_custos_magic_links.rb'
39
+ end
40
+
41
+ def create_api_tokens_migration
42
+ return unless plugins.include?('api_tokens')
43
+
44
+ migration_template 'create_custos_api_tokens.rb.tt',
45
+ 'db/migrate/create_custos_api_tokens.rb'
46
+ end
47
+
48
+ def create_mfa_credentials_migration
49
+ return unless plugins.include?('mfa')
50
+
51
+ migration_template 'create_custos_mfa_credentials.rb.tt',
52
+ 'db/migrate/create_custos_mfa_credentials.rb'
53
+ end
54
+
55
+ def create_remember_tokens_migration
56
+ return unless plugins.include?('remember_me')
57
+
58
+ migration_template 'create_custos_remember_tokens.rb.tt',
59
+ 'db/migrate/create_custos_remember_tokens.rb'
60
+ end
61
+
62
+ private
63
+
64
+ def table_name
65
+ model_name.underscore.tr('/', '_').pluralize
66
+ end
67
+
68
+ def migration_class_suffix
69
+ model_name.camelize.delete(':').pluralize
70
+ end
71
+
72
+ def column_plugins
73
+ %w[password lockout email_confirmation]
74
+ end
75
+
76
+ def has_password?
77
+ plugins.include?('password')
78
+ end
79
+
80
+ def has_lockout?
81
+ plugins.include?('lockout')
82
+ end
83
+
84
+ def has_email_confirmation?
85
+ plugins.include?('email_confirmation')
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,18 @@
1
+ class AddCustosTo<%= migration_class_suffix %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ <%- if has_password? -%>
4
+ add_column :<%= table_name %>, :password_digest, :string
5
+ <%- end -%>
6
+ <%- if has_lockout? -%>
7
+ add_column :<%= table_name %>, :failed_auth_count, :integer, default: 0, null: false
8
+ add_column :<%= table_name %>, :locked_at, :datetime
9
+ add_column :<%= table_name %>, :failed_mfa_count, :integer, default: 0, null: false
10
+ add_column :<%= table_name %>, :mfa_locked_at, :datetime
11
+ <%- end -%>
12
+ <%- if has_email_confirmation? -%>
13
+ add_column :<%= table_name %>, :email_confirmed_at, :datetime
14
+ add_column :<%= table_name %>, :email_confirmation_token_digest, :string
15
+ add_column :<%= table_name %>, :email_confirmation_sent_at, :datetime
16
+ <%- end -%>
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ class CreateCustosApiTokens < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :custos_api_tokens do |t|
4
+ t.string :authenticatable_type, null: false
5
+ t.bigint :authenticatable_id, null: false
6
+ t.string :token_digest, null: false
7
+ t.datetime :last_used_at
8
+ t.datetime :expires_at
9
+ t.datetime :revoked_at
10
+
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :custos_api_tokens, :token_digest, unique: true
15
+ add_index :custos_api_tokens, %i[authenticatable_type authenticatable_id],
16
+ name: "index_custos_api_tokens_on_authenticatable"
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ class CreateCustosMagicLinks < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :custos_magic_links do |t|
4
+ t.string :authenticatable_type, null: false
5
+ t.bigint :authenticatable_id, null: false
6
+ t.string :token_digest, null: false
7
+ t.datetime :expires_at, null: false
8
+ t.datetime :used_at
9
+
10
+ t.datetime :created_at, null: false
11
+ end
12
+
13
+ add_index :custos_magic_links, :token_digest, unique: true
14
+ add_index :custos_magic_links, %i[authenticatable_type authenticatable_id],
15
+ name: "index_custos_magic_links_on_authenticatable"
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ class CreateCustosMfaCredentials < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :custos_mfa_credentials do |t|
4
+ t.string :authenticatable_type, null: false
5
+ t.bigint :authenticatable_id, null: false
6
+ t.string :method, null: false
7
+ t.text :secret_data, null: false
8
+ t.datetime :enabled_at
9
+
10
+ t.timestamps
11
+ end
12
+
13
+ add_index :custos_mfa_credentials, %i[authenticatable_type authenticatable_id method],
14
+ unique: true, name: "index_custos_mfa_on_authenticatable_and_method"
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ class CreateCustosRememberTokens < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :custos_remember_tokens do |t|
4
+ t.string :authenticatable_type, null: false
5
+ t.bigint :authenticatable_id, null: false
6
+ t.string :token_digest, null: false
7
+ t.datetime :expires_at, null: false
8
+
9
+ t.datetime :created_at, null: false
10
+ end
11
+
12
+ add_index :custos_remember_tokens, :token_digest, unique: true
13
+ add_index :custos_remember_tokens, %i[authenticatable_type authenticatable_id],
14
+ name: "index_custos_remember_tokens_on_authenticatable"
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: custos
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ingvar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: argon2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '7.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '7.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rotp
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '6.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '6.0'
55
+ description: Modern, modular authentication for Rails applications. Supports password,
56
+ magic link, API tokens, MFA, and more — all as composable plugins.
57
+ email:
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ - lib/custos.rb
66
+ - lib/custos/authenticatable.rb
67
+ - lib/custos/callback_registry.rb
68
+ - lib/custos/configuration.rb
69
+ - lib/custos/controller_helpers.rb
70
+ - lib/custos/mfa_encryptor.rb
71
+ - lib/custos/model_config.rb
72
+ - lib/custos/models/api_token.rb
73
+ - lib/custos/models/magic_link_token.rb
74
+ - lib/custos/models/mfa_credential.rb
75
+ - lib/custos/models/remember_token.rb
76
+ - lib/custos/plugin.rb
77
+ - lib/custos/plugins/api_tokens.rb
78
+ - lib/custos/plugins/email_confirmation.rb
79
+ - lib/custos/plugins/lockout.rb
80
+ - lib/custos/plugins/magic_link.rb
81
+ - lib/custos/plugins/mfa.rb
82
+ - lib/custos/plugins/password.rb
83
+ - lib/custos/plugins/remember_me.rb
84
+ - lib/custos/railtie.rb
85
+ - lib/custos/session.rb
86
+ - lib/custos/session_manager.rb
87
+ - lib/custos/tasks/cleanup.rake
88
+ - lib/custos/token_generator.rb
89
+ - lib/custos/version.rb
90
+ - lib/generators/custos/install/install_generator.rb
91
+ - lib/generators/custos/install/templates/create_custos_sessions.rb.tt
92
+ - lib/generators/custos/install/templates/custos_initializer.rb.tt
93
+ - lib/generators/custos/model/model_generator.rb
94
+ - lib/generators/custos/model/templates/add_custos_columns.rb.tt
95
+ - lib/generators/custos/model/templates/create_custos_api_tokens.rb.tt
96
+ - lib/generators/custos/model/templates/create_custos_magic_links.rb.tt
97
+ - lib/generators/custos/model/templates/create_custos_mfa_credentials.rb.tt
98
+ - lib/generators/custos/model/templates/create_custos_remember_tokens.rb.tt
99
+ homepage: https://github.com/supostat/Custos
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ homepage_uri: https://github.com/supostat/Custos
104
+ source_code_uri: https://github.com/supostat/Custos
105
+ changelog_uri: https://github.com/supostat/Custos/blob/main/gem/CHANGELOG.md
106
+ documentation_uri: https://github.com/supostat/Custos
107
+ bug_tracker_uri: https://github.com/supostat/Custos/issues
108
+ rubygems_mfa_required: 'true'
109
+ allowed_push_host: https://rubygems.org
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '3.2'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubygems_version: 3.4.19
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Plugin-based authentication for Rails
129
+ test_files: []