Class: Mcp::Auth::Services::TokenService
- Inherits:
-
Object
- Object
- Mcp::Auth::Services::TokenService
- Defined in:
- lib/mcp/auth/services/token_service.rb
Class Method Summary collapse
-
.generate_access_token(data, base_url:) ⇒ Object
Generate JWT access token with proper audience binding.
-
.generate_refresh_token(data) ⇒ Object
Generate refresh token.
-
.generate_token_response(data, base_url:) ⇒ Object
Generate complete token response.
-
.revoke_refresh_token(refresh_token) ⇒ Object
Revoke refresh token (RFC 7009).
-
.signing_jwk_export ⇒ Object
JWK for the active public key, suitable for the JWKS endpoint.
-
.signing_kid ⇒ Object
JWK identifier for the current signing key — included as ‘kid` in JWT headers and the JWKS entry.
-
.signing_public_key ⇒ Object
Public key used to sign new JWTs.
-
.validate_access_token(token, resource: nil) ⇒ Object
Validate access token with optional resource verification (RFC 8707).
-
.validate_refresh_token(refresh_token) ⇒ Object
Validate refresh token.
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.}" 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.}" 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.}" 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_export ⇒ Object
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_kid ⇒ Object
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_key ⇒ Object
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.}" nil rescue StandardError => e Rails.logger.error "[TokenService] Token validation error: #{e.}" 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 |