tokenzen 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: 2f4dde7ef56bd6c8ed1d4b23b43f4256b17dfce96f9a0b8a0272e294db5fbf30
4
+ data.tar.gz: c8fbddcc5c83663e8635063fc67dd0d48b085166ce1c8b8f13f3f5c01266bbe5
5
+ SHA512:
6
+ metadata.gz: 13f223728ffc56a0609a1834b09c1e1bb4b5dc0d6d4a7a426a221d9d42bd316a52cea0135ef11d9430943057ae43ec67c2b5e8c98a624a624e3209e9b389100e
7
+ data.tar.gz: 4b38a201f3a87dd92e0c8c68d4eed112ff497f960c10cab5ebea806843e430b21d1c93f85247974649956bd8915701d1971c96ad442e9d461af19b928a367adb
data/README.md ADDED
@@ -0,0 +1,210 @@
1
+ Tokenzen
2
+ =======
3
+
4
+ Tokenzen is a lightweight, model-agnostic token authentication toolkit for Rails.
5
+
6
+ It provides secure, polymorphic access token management for any ActiveRecord model — not just User.
7
+
8
+ Tokenzen is designed to be:
9
+ - Model agnostic
10
+ - Multi-model compatible
11
+ - Cache-backed and scalable
12
+ - Configurable
13
+ - Lightweight
14
+ - Easy to integrate
15
+
16
+ ========================================
17
+ FEATURES
18
+ =========
19
+
20
+ - Works with any model (Admin, Customer, Account, etc.)
21
+ - Access and refresh token generation
22
+ - Token revocation (logout)
23
+ - Multi-device support
24
+ - Automatic token rotation on refresh token change
25
+ - Token validation
26
+ - Refresh access token using refresh token
27
+ - Configurable expiration
28
+ - AES-256 encryption of tokens
29
+ - Rails auto-loading via Railtie
30
+
31
+ ========================================
32
+ INSTALLATION
33
+ ============
34
+
35
+ Add this to your application's Gemfile:
36
+
37
+ gem "tokenzen"
38
+
39
+ Then run:
40
+
41
+ bundle install
42
+
43
+ Or install manually:
44
+
45
+ gem install tokenzen
46
+
47
+ ========================================
48
+ BASIC USAGE
49
+ ===========
50
+
51
+ Include Tokenzen in any ActiveRecord model.
52
+
53
+ Example:
54
+
55
+ class Admin < ApplicationRecord
56
+ include Tokenzen::Authenticatable
57
+ tokenzen
58
+ end
59
+
60
+ You can use Tokenzen in multiple models at the same time:
61
+
62
+ class Customer < ApplicationRecord
63
+ include Tokenzen::Authenticatable
64
+ tokenzen
65
+ end
66
+
67
+ ========================================
68
+ GENERATE ACCESS + REFRESH TOKEN
69
+ ===============================
70
+
71
+ admin = Admin.first
72
+ tokens = admin.generate_tokens
73
+ # tokens => { access_token: "...", refresh_token: "..." }
74
+
75
+ This generates secure encrypted tokens and stores them in cache with separate expiries.
76
+
77
+ ========================================
78
+ LOGIN / VALIDATE TOKENS
79
+ =======================
80
+
81
+ Use the class-level `login` method to validate a token pair:
82
+
83
+ record = Admin.login(tokens[:access_token], tokens[:refresh_token])
84
+ # returns Admin instance if valid, nil if invalid
85
+
86
+ You can also validate token pair from an instance:
87
+
88
+ valid = admin.validate_tokens(tokens[:access_token])
89
+ # => ActiveRecord or nil
90
+
91
+ ========================================
92
+ REFRESH ACCESS TOKEN
93
+ ====================
94
+
95
+ Use the refresh token to generate a new access token:
96
+
97
+ new_tokens = Admin.refresh_access(tokens[:refresh_token])
98
+ # returns { access_token: "...", refresh_token: "..." }
99
+
100
+ ========================================
101
+ LOGOUT / CLEAR ALL TOKENS
102
+ =========================
103
+
104
+ admin.logout
105
+ # clears all access and refresh tokens for this record
106
+
107
+ ========================================
108
+ CONFIGURATION
109
+ =============
110
+
111
+ Create an initializer:
112
+
113
+ config/initializers/tokenzen.rb
114
+
115
+ Tokenzen.configure do |config|
116
+ config.access_token_expiry = 2.days
117
+ config.refresh_token_expiry = 2.months
118
+ config.secret_key = ENV["TOKENZEN_SECRET_KEY"] || Rails.application.secret_key_base
119
+ end
120
+
121
+ The gem automatically encrypts all tokens using AES-256 with this secret key.
122
+
123
+ ========================================
124
+ HOW IT WORKS
125
+ =============
126
+
127
+ When a token is issued:
128
+
129
+ - A secure random key is generated for access and refresh tokens.
130
+ - Tokens are stored in cache along with model name and record ID.
131
+ - Tokens are encrypted using AES-256.
132
+ - Tokens are tracked per record for easy revocation.
133
+
134
+ Stored payload example:
135
+
136
+ {
137
+ "model" => "Admin",
138
+ "id" => 1,
139
+ "type" => "access" # or "refresh"
140
+ }
141
+
142
+ This allows Tokenzen to work with any ActiveRecord model automatically.
143
+
144
+ ========================================
145
+ TOKEN ROTATION
146
+ ===============
147
+
148
+ If your model includes:
149
+
150
+ - refresh_token
151
+ - password_digest
152
+
153
+ Tokenzen will automatically:
154
+
155
+ - Rotate the refresh token when password changes
156
+ - Clear all access tokens when refresh token changes
157
+
158
+ This behavior is optional and safely guarded.
159
+
160
+ ========================================
161
+ PRODUCTION RECOMMENDATION
162
+ ==========================
163
+
164
+ Use Redis as your cache store for production environments:
165
+
166
+ config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }
167
+
168
+ Redis provides better performance and scalability for token storage.
169
+
170
+ ========================================
171
+ REQUIREMENTS
172
+ =============
173
+
174
+ - Ruby >= 3.0.0
175
+ - Rails >= 6.0
176
+ - ActiveRecord-backed models
177
+
178
+ ========================================
179
+ SECURITY NOTES
180
+ ================
181
+
182
+ - Tokens are encrypted using AES-256
183
+ - Tokens are stored in cache (Redis recommended)
184
+ - Tokens are namespaced per model
185
+ - Clearing tokens invalidates all sessions instantly
186
+
187
+ ========================================
188
+ ROADMAP
189
+ ========
190
+
191
+ - Controller helpers (current_resource)
192
+ - Rack middleware
193
+ - Stateless JWT mode
194
+ - Device/session tracking
195
+ - Revokable single-session tokens
196
+ - OAuth support
197
+
198
+ ========================================
199
+ CONTRIBUTING
200
+ ==============
201
+
202
+ Bug reports and pull requests are welcome at:
203
+
204
+ https://github.com/stndrk/tokenzen
205
+
206
+ ========================================
207
+ LICENSE
208
+ ========
209
+
210
+ Tokenzen is released under the MIT License.
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tokenzen
4
+ module Authenticatable
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ def tokenzen(options = {})
11
+ include Tokenzen::Authenticatable::InstanceMethods
12
+
13
+ class_attribute :tokenzen_options
14
+ self.tokenzen_options = options
15
+
16
+ before_save :_tokenzen_rotate_refresh_token
17
+ around_save :_tokenzen_handle_refresh_rotation
18
+ after_destroy :_tokenzen_clear_tokens
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ attr_reader :current_access_token, :current_refresh_token
24
+
25
+ # Generate access + refresh token
26
+ def generate_tokens(access_expiry: nil, refresh_expiry: nil)
27
+ tokens = TokenStore.issue_tokens(self, access_expiry: access_expiry, refresh_expiry: refresh_expiry)
28
+ @current_access_token = tokens[:access_token]
29
+ @current_refresh_token = tokens[:refresh_token]
30
+ tokens
31
+ end
32
+
33
+ # Logout: clear all tokens
34
+ def logout
35
+ clear_all_access_tokens
36
+ end
37
+
38
+ # Clear all tokens
39
+ def clear_all_access_tokens
40
+ TokenStore.clear_tokens(self)
41
+ @current_access_token = nil
42
+ @current_refresh_token = nil
43
+ end
44
+
45
+ # Validate tokens for this record
46
+ def validate_tokens(access_token)
47
+ TokenStore.fetch_record_from_access(access_token)
48
+ end
49
+
50
+ # =============================
51
+ # Class-level login using token pair
52
+ # =============================
53
+ #
54
+ # Returns the record if tokens valid, else nil
55
+ #
56
+ def self.login(access_token, refresh_token = nil)
57
+ record = TokenStore.fetch_record_from_access(access_token)
58
+ return nil unless record
59
+
60
+ if refresh_token
61
+ refresh_record = TokenStore.fetch_record_from_refresh(refresh_token)
62
+ return nil unless refresh_record == record
63
+ end
64
+
65
+ record
66
+ end
67
+
68
+ # Refresh access token using refresh token
69
+ def self.refresh_access(refresh_token)
70
+ TokenStore.refresh_access_token(refresh_token)
71
+ end
72
+
73
+ private
74
+
75
+ def _tokenzen_rotate_refresh_token
76
+ return unless respond_to?(:password_digest_changed?) && password_digest_changed?
77
+ self.refresh_token = JwtProcessor.encode(model: self.class.name, id: id) if respond_to?(:refresh_token=)
78
+ end
79
+
80
+ def _tokenzen_handle_refresh_rotation
81
+ changed = respond_to?(:refresh_token_changed?) && refresh_token_changed?
82
+ yield
83
+ return unless changed
84
+ clear_all_access_tokens
85
+ generate_tokens
86
+ end
87
+
88
+ def _tokenzen_clear_tokens
89
+ clear_all_access_tokens
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/message_encryptor"
5
+ require "active_support/key_generator"
6
+
7
+ module Tokenzen
8
+ class Configuration
9
+ attr_accessor :cache_store,
10
+ :access_token_expiry,
11
+ :refresh_token_expiry,
12
+ :secret_key
13
+
14
+ def initialize
15
+ @cache_store = Rails.cache
16
+ @access_token_expiry = 2.days
17
+ @refresh_token_expiry = 2.months
18
+
19
+ @secret_key = default_secret_key
20
+ end
21
+
22
+ # Public encrypt method
23
+ def encrypt(value)
24
+ message_encryptor.encrypt_and_sign(value)
25
+ end
26
+
27
+ # Public decrypt method
28
+ def decrypt(value)
29
+ message_encryptor.decrypt_and_verify(value)
30
+ end
31
+
32
+ private
33
+
34
+ def message_encryptor
35
+ @message_encryptor ||= begin
36
+ key_len = ActiveSupport::MessageEncryptor.key_len
37
+ generator = ActiveSupport::KeyGenerator.new(@secret_key)
38
+ key = generator.generate_key("tokenzen", key_len)
39
+
40
+ ActiveSupport::MessageEncryptor.new(key)
41
+ end
42
+ end
43
+
44
+ def default_secret_key
45
+ if defined?(Rails) && Rails.application.respond_to?(:secret_key_base)
46
+ Rails.application.secret_key_base
47
+ else
48
+ ENV["TOKENZEN_SECRET_KEY"] || raise("TOKENZEN_SECRET_KEY not set in ENV and Rails.secret_key_base not found")
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tokenzen
4
+ module Model
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attr_reader :current_access_token
9
+
10
+ before_save :renew_refresh_token_on_password_change
11
+ around_save :handle_refresh_token_rotation
12
+ after_destroy :clear_all_access_tokens
13
+ end
14
+
15
+ # PUBLIC API
16
+ def generate_access_token
17
+ token = Tokenzen::TokenStore.write(id)
18
+ return unless token
19
+
20
+ Tokenzen::TokenStore.store_user_token(id, token)
21
+ @current_access_token = token
22
+ end
23
+
24
+ def clear_all_access_tokens
25
+ Tokenzen::TokenStore.clear_user_tokens(id)
26
+ @current_access_token = nil
27
+ end
28
+
29
+ private
30
+
31
+ def renew_refresh_token_on_password_change
32
+ return unless respond_to?(:password_digest_changed?) && password_digest_changed?
33
+
34
+ self.refresh_token = JwtProcessor.encode(id: id)
35
+ end
36
+
37
+ def handle_refresh_token_rotation
38
+ changed = respond_to?(:refresh_token_changed?) && refresh_token_changed?
39
+ yield
40
+ return unless changed
41
+
42
+ clear_all_access_tokens
43
+ generate_access_token
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tokenzen
4
+ class Railtie < Rails::Railtie
5
+ initializer "tokenzen.initialize" do
6
+ Tokenzen.configuration ||= Tokenzen::Configuration.new
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tokenzen
4
+ class TokenStore
5
+ class << self
6
+
7
+ # Generate both tokens and store in cache
8
+ def issue_tokens(record, access_expiry: nil, refresh_expiry: nil)
9
+ access_expiry ||= Tokenzen.configuration.access_token_expiry
10
+ refresh_expiry ||= Tokenzen.configuration.refresh_token_expiry
11
+
12
+ access_key = SecureRandom.hex(32)
13
+ refresh_key = SecureRandom.hex(32)
14
+
15
+ access_payload = {
16
+ "model" => record.class.name,
17
+ "id" => record.id,
18
+ "type" => "access"
19
+ }
20
+
21
+ refresh_payload = {
22
+ "model" => record.class.name,
23
+ "id" => record.id,
24
+ "type" => "refresh"
25
+ }
26
+
27
+ cache.write(access_key, access_payload, expires_in: access_expiry)
28
+ cache.write(refresh_key, refresh_payload, expires_in: refresh_expiry)
29
+
30
+ register_token(record, access_key)
31
+ register_token(record, refresh_key)
32
+
33
+ {
34
+ access_token: encrypt(access_key),
35
+ refresh_token: encrypt(refresh_key)
36
+ }
37
+ end
38
+
39
+ # Fetch record using access token
40
+ def fetch_record_from_access(token)
41
+ raw_key = decrypt(token)
42
+ payload = cache.read(raw_key)
43
+ return nil unless payload && payload["type"] == "access"
44
+
45
+ payload["model"].constantize.find_by(id: payload["id"])
46
+ end
47
+
48
+ # Fetch record using refresh token
49
+ def fetch_record_from_refresh(token)
50
+ raw_key = decrypt(token)
51
+ payload = cache.read(raw_key)
52
+ return nil unless payload && payload["type"] == "refresh"
53
+
54
+ payload["model"].constantize.find_by(id: payload["id"])
55
+ end
56
+
57
+ # Refresh access token using refresh token
58
+ def refresh_access_token(refresh_token)
59
+ record = fetch_record_from_refresh(refresh_token)
60
+ return unless record
61
+
62
+ tokens = issue_tokens(record)
63
+ clear_tokens(record) # clear old tokens
64
+ tokens
65
+ end
66
+
67
+ # Clear all tokens for a record
68
+ def clear_tokens(record)
69
+ tokens_for(record).each { |key| cache.delete(key) }
70
+ cache.write(register_key(record), [], expires_in: Tokenzen.configuration.refresh_token_expiry)
71
+ end
72
+
73
+ private
74
+
75
+ def register_token(record, raw_key)
76
+ list = tokens_for(record)
77
+ list << raw_key
78
+ cache.write(register_key(record), list, expires_in: Tokenzen.configuration.refresh_token_expiry)
79
+ end
80
+
81
+ def tokens_for(record)
82
+ cache.read(register_key(record)) || []
83
+ end
84
+
85
+ def register_key(record)
86
+ "tokenzen:#{record.class.name}:#{record.id}:tokens"
87
+ end
88
+
89
+ def cache
90
+ Tokenzen.configuration.cache_store
91
+ end
92
+
93
+ def encrypt(value)
94
+ Tokenzen.configuration.encrypt(value)
95
+ end
96
+
97
+ def decrypt(value)
98
+ Tokenzen.configuration.decrypt(value)
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tokenzen
4
+ VERSION = "0.1.0"
5
+ end
data/lib/tokenzen.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ require_relative "tokenzen/version"
6
+ require_relative "tokenzen/configuration"
7
+ require_relative "tokenzen/token_store"
8
+ require_relative "tokenzen/authenticatable"
9
+ require_relative "tokenzen/railtie"
10
+
11
+ module Tokenzen
12
+ class Error < StandardError; end
13
+
14
+ class << self
15
+ attr_accessor :configuration
16
+
17
+ def configure
18
+ self.configuration ||= Configuration.new
19
+ yield(configuration)
20
+ end
21
+
22
+ def configuration
23
+ @configuration ||= Configuration.new
24
+ end
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tokenzen
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Satendra
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-02-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ description: Tokenzen is a lightweight, model-agnostic authentication toolkit for
42
+ Rails. It provides secure, polymorphic access token management for any ActiveRecord
43
+ model with configurable expiration, AES-256 encryption, login, logout, and refresh
44
+ token rotation.
45
+ email:
46
+ - satendra.km@alumni.iitd.ac.in
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - README.md
52
+ - lib/tokenzen.rb
53
+ - lib/tokenzen/authenticatable.rb
54
+ - lib/tokenzen/configuration.rb
55
+ - lib/tokenzen/model.rb
56
+ - lib/tokenzen/railtie.rb
57
+ - lib/tokenzen/token_store.rb
58
+ - lib/tokenzen/version.rb
59
+ homepage: https://github.com/stndrk/tokenzen
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ rubygems_mfa_required: 'true'
64
+ allowed_push_host: https://rubygems.org
65
+ homepage_uri: https://github.com/stndrk/tokenzen
66
+ source_code_uri: https://github.com/stndrk/tokenzen
67
+ changelog_uri: https://github.com/stndrk/tokenzen/blob/main/CHANGELOG.md
68
+ bug_tracker_uri: https://github.com/stndrk/tokenzen/issues
69
+ documentation_uri: https://github.com/stndrk/tokenzen#readme
70
+ post_install_message:
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: 3.0.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: 3.5.21
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Model-agnostic token authentication for Rails
89
+ test_files: []