Class: KairosMcp::Auth::TokenStore
- Inherits:
-
Object
- Object
- KairosMcp::Auth::TokenStore
- Defined in:
- lib/kairos_mcp/auth/token_store.rb
Overview
TokenStore: Manages Bearer tokens for HTTP authentication
Stores SHA256 hashes of tokens (never raw tokens). Supports file-based (JSON) storage. SQLite support can be added later by extending Storage::Backend.
Token lifecycle:
1. --init-admin generates the first owner token (CLI)
2. owner uses token_manage tool to create/revoke/rotate tokens
3. Tokens expire after configured period (default: 90 days)
Token data structure:
{
token_hash: "sha256...",
user: "username",
role: "owner" | "member" | "guest",
issued_at: "2026-02-12T10:00:00Z",
expires_at: "2026-05-13T10:00:00Z" | nil,
issued_by: "username" | "system",
status: "active" | "revoked"
}
Roles (Phase 1: all roles available, authorization enforced in Phase 2):
- owner: Full access, can manage tokens
- member: Standard team access (Phase 2: L1/L2 write, L0 read)
- guest: Limited access (Phase 2: read-only, own L2 only)
Constant Summary collapse
- VALID_ROLES =
%w[owner member guest].freeze
- TOKEN_PREFIX =
'kc_'- DEFAULT_EXPIRY_DAYS =
90
Instance Attribute Summary collapse
-
#store_path ⇒ Object
readonly
.
Class Method Summary collapse
-
.create(config = {}) ⇒ Object
Factory: create a TokenStore based on config.
-
.register(name, klass) ⇒ Object
Register a named TokenStore backend (e.g. ‘postgresql’).
- .unregister(name) ⇒ Object
Instance Method Summary collapse
-
#create(user:, role: 'member', issued_by: 'system', expires_in: nil, pubkey_hash: nil) ⇒ Hash
Generate a new token for a user.
-
#empty? ⇒ Boolean
Check if any tokens exist.
-
#initialize(store_path = nil) ⇒ TokenStore
constructor
A new instance of TokenStore.
-
#list(include_revoked: false) ⇒ Array<Hash>
List all tokens (without hashes, for display).
-
#reload! ⇒ Object
Reload tokens from disk.
-
#revoke(user:) ⇒ Integer
Revoke a user’s token(s).
-
#rotate(user:, issued_by: 'system') ⇒ Hash
Rotate a user’s token (revoke old, create new).
-
#verify(raw_token) ⇒ Hash?
Verify a raw token and return user info.
Constructor Details
#initialize(store_path = nil) ⇒ TokenStore
Returns a new instance of TokenStore.
73 74 75 76 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 73 def initialize(store_path = nil) @store_path = store_path || default_store_path @tokens = load_tokens end |
Instance Attribute Details
#store_path ⇒ Object (readonly)
71 72 73 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 71 def store_path @store_path end |
Class Method Details
.create(config = {}) ⇒ Object
Factory: create a TokenStore based on config. If a SkillSet has registered a backend matching config, use that; otherwise fall back to file-based store.
61 62 63 64 65 66 67 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 61 def self.create(config = {}) backend = config[:backend]&.to_s if backend && @registry.key?(backend) return @registry[backend].new(config[backend.to_sym] || {}) end new(config[:store_path]) end |
.register(name, klass) ⇒ Object
Register a named TokenStore backend (e.g. ‘postgresql’)
50 51 52 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 50 def self.register(name, klass) @registry[name.to_s] = klass end |
.unregister(name) ⇒ Object
54 55 56 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 54 def self.unregister(name) @registry.delete(name.to_s) end |
Instance Method Details
#create(user:, role: 'member', issued_by: 'system', expires_in: nil, pubkey_hash: nil) ⇒ Hash
Generate a new token for a user
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 86 def create(user:, role: 'member', issued_by: 'system', expires_in: nil, pubkey_hash: nil) validate_role!(role) validate_user!(user) raw_token = generate_token token_hash = hash_token(raw_token) now = Time.now expires_at = calculate_expiry(now, expires_in) entry = { 'token_hash' => token_hash, 'user' => user, 'role' => role, 'issued_at' => now.iso8601, 'expires_at' => expires_at&.iso8601, 'issued_by' => issued_by, 'status' => 'active' } entry['pubkey_hash'] = pubkey_hash if pubkey_hash @tokens << entry save_tokens entry.merge('raw_token' => raw_token) end |
#empty? ⇒ Boolean
Check if any tokens exist
198 199 200 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 198 def empty? @tokens.empty? || @tokens.none? { |e| e['status'] == 'active' } end |
#list(include_revoked: false) ⇒ Array<Hash>
List all tokens (without hashes, for display)
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 178 def list(include_revoked: false) entries = @tokens entries = entries.select { |e| e['status'] == 'active' } unless include_revoked entries.map do |entry| { user: entry['user'], role: entry['role'], status: entry['status'], issued_at: entry['issued_at'], expires_at: entry['expires_at'], issued_by: entry['issued_by'], expired: expired?(entry) } end end |
#reload! ⇒ Object
Reload tokens from disk
203 204 205 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 203 def reload! @tokens = load_tokens end |
#revoke(user:) ⇒ Integer
Revoke a user’s token(s)
139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 139 def revoke(user:) count = 0 @tokens.each do |entry| if entry['user'] == user && entry['status'] == 'active' entry['status'] = 'revoked' count += 1 end end save_tokens if count > 0 count end |
#rotate(user:, issued_by: 'system') ⇒ Hash
Rotate a user’s token (revoke old, create new)
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 157 def rotate(user:, issued_by: 'system') old_entry = @tokens.find { |e| e['user'] == user && e['status'] == 'active' } role = old_entry ? old_entry['role'] : 'member' expires_in = nil if old_entry && old_entry['expires_at'] # Preserve the same expiry duration original_issued = Time.parse(old_entry['issued_at']) original_expires = Time.parse(old_entry['expires_at']) duration_seconds = (original_expires - original_issued).to_i expires_in = "#{duration_seconds / 86400}d" end revoke(user: user) create(user: user, role: role, issued_by: issued_by, expires_in: expires_in) end |
#verify(raw_token) ⇒ Hash?
Verify a raw token and return user info
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/kairos_mcp/auth/token_store.rb', line 117 def verify(raw_token) token_hash = hash_token(raw_token) entry = find_by_hash(token_hash) return nil unless entry return nil if entry['status'] != 'active' return nil if expired?(entry) result = { user: entry['user'], role: entry['role'], issued_at: entry['issued_at'], expires_at: entry['expires_at'] } result[:pubkey_hash] = entry['pubkey_hash'] if entry['pubkey_hash'] result end |