Module: McpToolkit::Authority::Token

Extended by:
ActiveSupport::Concern
Defined in:
lib/mcp_toolkit/authority/token.rb

Overview

Generic, ownership-agnostic machinery for an AUTHORITY's access-token model, packaged as a concern to include into a host's ActiveRecord token model. It extracts everything about a token that is NOT specific to the host's tenancy model (how a token maps to accounts/users), leaving the host to declare only its ownership associations, account resolution, and any bespoke validations.

Expected columns on the including model:

token_digest  :string   NOT NULL, unique — SHA256(plaintext) for O(1) lookup
token_prefix  :string   NOT NULL — first 11 plaintext chars, safe to display
scopes        :string[] / json — OAuth-style "<app>__<action>" grants (nullable)
expires_at    :datetime nullable — nil = never expires
last_used_at  :datetime nullable — throttled touch (see #touch_last_used!)

What it provides:

* Secure token generation on create (`assign_token`) + the plaintext reader
(`#token`, populated only on the instance that generated it).
* Lookup/verification: `.authenticate(plaintext)` / `.digest_for(plaintext)`.
* Lifecycle scopes (`active` / `expired` / `never_used` / `used_within`) and
`#expired?`, plus the throttled `#touch_last_used!`.
* OAuth-style scope helpers (`#normalized_scopes` / `#authorized_for_scope?` /
`#scope_restricted?`).

Why a plain SHA256 (not bcrypt/scrypt/argon2): the token is a high-entropy random secret (24 bytes over a 64-char alphabet ≈ 144 bits), so rainbow tables can't exist and brute force is infeasible — the slow KDFs that protect low-entropy passwords buy nothing and would break the O(1) find_by(token_digest:) lookup.

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

TOKEN_PREFIX =

Plaintext token layout. "mcp_" is the MCP-generic scheme prefix (not a host fingerprint); the display length is that prefix plus 7 random chars.

"mcp_"
RAW_TOKEN_BYTES =
24
TOKEN_PREFIX_DISPLAY_LENGTH =
11
LAST_USED_AT_THROTTLE =

Skip the last_used_at UPDATE unless this much time has passed, so a burst of calls doesn't write on every request.

1.minute

Instance Method Summary collapse

Instance Method Details

#authorized_for_scope?(scope) ⇒ Boolean

Per-scope check. A tool requiring no scope is reachable by any token; a tool requiring a scope needs the token to HOLD that exact scope. An unrestricted token holds NO scopes, so it can reach only no-scope tools.

Returns:

  • (Boolean)


93
94
95
96
97
# File 'lib/mcp_toolkit/authority/token.rb', line 93

def authorized_for_scope?(scope)
  return true if scope.blank?

  normalized_scopes.include?(scope.to_s)
end

#expired?Boolean

Returns:

  • (Boolean)


80
81
82
# File 'lib/mcp_toolkit/authority/token.rb', line 80

def expired?
  expires_at.present? && expires_at <= Time.current
end

#normalized_scopesObject

The OAuth-style scopes granted to this token as a clean array of "__" strings. An unrestricted token (NULL/empty scopes) returns [].



86
87
88
# File 'lib/mcp_toolkit/authority/token.rb', line 86

def normalized_scopes
  Array(scopes).compact_blank
end

#reloadObject



75
76
77
78
# File 'lib/mcp_toolkit/authority/token.rb', line 75

def reload(...)
  @token = nil
  super
end

#scope_restricted?Boolean

True when the token carries an explicit scope set (i.e. is restricted).

Returns:

  • (Boolean)


100
101
102
# File 'lib/mcp_toolkit/authority/token.rb', line 100

def scope_restricted?
  normalized_scopes.any?
end

#touch_last_used!Object

Throttled last_used_at bump: persists on its own, without validations or bumping updated_at, and only once per LAST_USED_AT_THROTTLE window.



106
107
108
109
110
111
# File 'lib/mcp_toolkit/authority/token.rb', line 106

def touch_last_used!
  return unless last_used_at.nil? || last_used_at < LAST_USED_AT_THROTTLE.ago

  self.last_used_at = Time.current
  save!(validate: false, touch: false)
end