Class: Mcp::Auth::Services::TokenService

Inherits:
Object
  • Object
show all
Defined in:
lib/mcp/auth/services/token_service.rb

Class Method Summary collapse

Class Method Details

.generate_access_token(data, base_url:) ⇒ Object

Generate JWT access token with proper audience binding



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/mcp/auth/services/token_service.rb', line 40

def generate_access_token(data, base_url:)
  user_data = fetch_user_data(data)

  # RFC 8707: Use provided resource or default to MCP API endpoint
  audience = normalize_resource_uri(data[:resource].presence || "#{base_url}/mcp")

  # Calculate expiration time
  exp_time = data[:expires_at] ? data[:expires_at].to_i : (Time.current.to_i + token_lifetime)

  payload = {
    iss: base_url,
    aud: audience,
    sub: data[:user_id].to_s,
    org: data[:org_id]&.to_s,
    client_id: data[:client_id],
    email: user_data[:email],
    scope: data[:scope],
    api_key_id: user_data[:api_key_id],
    api_key_secret: user_data[:api_key_secret],
    iat: Time.current.to_i,
    exp: exp_time
  }

  jwt_headers = signing_kid ? { kid: signing_kid } : {}
  token = JWT.encode(payload, signing_key, signing_algorithm, jwt_headers)

  # Store token in database for revocation support
  store_access_token(token, data, audience)

  token
rescue StandardError => e
  Rails.logger.error "[TokenService] Failed to generate access token: #{e.message}"
  raise
end

.generate_refresh_token(data) ⇒ Object

Generate refresh token



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/mcp/auth/services/token_service.rb', line 76

def generate_refresh_token(data)
  refresh_token = SecureRandom.hex(32)

  # Use provided expires_at or default
  expires_at = data[:expires_at] || refresh_token_lifetime.seconds.from_now

  begin
    Mcp::Auth::RefreshToken.create!(
      token: refresh_token,
      client_id: data[:client_id],
      scope: data[:scope],
      user_id: data[:user_id],
      org_id: data[:org_id],
      expires_at: expires_at
    )

    Rails.logger.info "[TokenService] Refresh token created for user #{data[:user_id]}"
    refresh_token
  rescue ActiveRecord::RecordInvalid => e
    Rails.logger.error "[TokenService] Failed to create refresh token: #{e.message}"
    nil
  end
end

.generate_token_response(data, base_url:) ⇒ Object

Generate complete token response



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/mcp/auth/services/token_service.rb', line 132

def generate_token_response(data, base_url:)
  access_token = generate_access_token(data, base_url: base_url)
  refresh_token = generate_refresh_token(data)

  response = {
    access_token: access_token,
    token_type: 'Bearer',
    expires_in: token_lifetime,
    scope: data[:scope]
  }

  response[:refresh_token] = refresh_token if refresh_token
  response
rescue StandardError => e
  Rails.logger.error "[TokenService] Failed to generate token response: #{e.message}"
  raise
end

.revoke_refresh_token(refresh_token) ⇒ Object

Revoke refresh token (RFC 7009)



120
121
122
123
124
125
126
127
128
129
# File 'lib/mcp/auth/services/token_service.rb', line 120

def revoke_refresh_token(refresh_token)
  return false if refresh_token.blank?

  token_record = Mcp::Auth::RefreshToken.find_by(token: refresh_token)
  return false unless token_record

  token_record.destroy
  Rails.logger.info "[TokenService] Refresh token revoked"
  true
end

.signing_jwk_exportObject

JWK for the active public key, suitable for the JWKS endpoint. Returns nil for HS256 (HMAC keys are never published).



169
170
171
172
173
174
175
176
177
# File 'lib/mcp/auth/services/token_service.rb', line 169

def signing_jwk_export
  return nil unless asymmetric_signing?

  exported = jwk.export
  exported[:kid] = signing_kid
  exported[:alg] = signing_algorithm
  exported[:use] = 'sig'
  exported
end

.signing_kidObject

JWK identifier for the current signing key — included as ‘kid` in JWT headers and the JWKS entry. Falls back to JWT::JWK’s auto-derived thumbprint when no explicit kid is configured.



161
162
163
164
165
# File 'lib/mcp/auth/services/token_service.rb', line 161

def signing_kid
  return nil unless asymmetric_signing?

  Mcp::Auth.configuration&.token_signing_kid.presence || jwk.kid
end

.signing_public_keyObject

Public key used to sign new JWTs. Configured via Mcp::Auth.configure; built lazily so apps that stay on HS256 don’t have to set anything.



152
153
154
155
156
# File 'lib/mcp/auth/services/token_service.rb', line 152

def signing_public_key
  return nil unless asymmetric_signing?

  cached_public_key
end

.validate_access_token(token, resource: nil) ⇒ Object

Validate access token with optional resource verification (RFC 8707). Supports HS256, RS256, and ES256 — algorithm comes from configuration.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/mcp/auth/services/token_service.rb', line 10

def validate_access_token(token, resource: nil)
  return nil if token.blank?

  begin
    payload = JWT.decode(token, verification_key, true, { algorithm: signing_algorithm }).first

    # Check expiration manually to ensure proper handling
    if payload['exp']
      return nil if payload['exp'] <= Time.current.to_i
    end

    # Validate audience if resource provided (RFC 8707 compliance)
    if resource && payload['aud'].present?
      unless audience_matches?(payload['aud'], resource)
        Rails.logger.warn "[TokenService] Token audience mismatch: expected #{resource}, got #{payload['aud']}"
        return nil
      end
    end

    payload.symbolize_keys
  rescue JWT::DecodeError, JWT::ExpiredSignature => e
    Rails.logger.debug "[TokenService] Token validation failed: #{e.message}"
    nil
  rescue StandardError => e
    Rails.logger.error "[TokenService] Token validation error: #{e.message}"
    nil
  end
end

.validate_refresh_token(refresh_token) ⇒ Object

Validate refresh token



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/mcp/auth/services/token_service.rb', line 101

def validate_refresh_token(refresh_token)
  return nil if refresh_token.blank?

  token_record = Mcp::Auth::RefreshToken.find_by(token: refresh_token)
  return nil unless token_record

  # Check if token is expired
  return nil if token_record.expires_at < Time.current

  Rails.logger.info "[TokenService] Refresh token validated for user #{token_record.user_id}"
  {
    client_id: token_record.client_id,
    scope: token_record.scope,
    user_id: token_record.user_id,
    org_id: token_record.org_id
  }
end