Class: AgentAdmit::IntrospectionClient
- Inherits:
-
Object
- Object
- AgentAdmit::IntrospectionClient
- Defined in:
- lib/agentadmit/introspection_client.rb
Overview
Mandatory introspection client — validates tokens via AgentAdmit hosted service. No local JWT decode. Every verification call goes through AgentAdmit.
Defined Under Namespace
Classes: IntrospectionResult
Instance Method Summary collapse
-
#initialize(config = nil) ⇒ IntrospectionClient
constructor
A new instance of IntrospectionClient.
-
#verify(token) ⇒ IntrospectionResult
Validate an ag_at_ token via introspection.
Constructor Details
#initialize(config = nil) ⇒ IntrospectionClient
Returns a new instance of IntrospectionClient.
20 21 22 23 |
# File 'lib/agentadmit/introspection_client.rb', line 20 def initialize(config = nil) @config = config || AgentAdmit.configuration || Config.new @config.validate_api_key! end |
Instance Method Details
#verify(token) ⇒ IntrospectionResult
Validate an ag_at_ token via introspection.
Automatically retries on HTTP 429 with exponential backoff + jitter. Raises RateLimitError when retries are exhausted.
37 38 39 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 74 75 76 77 78 79 80 81 82 83 84 85 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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/agentadmit/introspection_client.rb', line 37 def verify(token) unless token.start_with?(@config.token_prefix_access) raise InvalidTokenError, "Not an AgentAdmit access token" end max_retries = @config.respond_to?(:max_retries) ? @config.max_retries.to_i : 3 delay_ms = 1000 # initial backoff in milliseconds uri = URI.parse(@config.verify_url) http = build_http(uri) (0..max_retries).each do |attempt| request = build_request(uri, token) begin response = http.request(request) rescue StandardError => e raise IntrospectionError, "Introspection failed: #{e.}" end status = response.code.to_i if status == 429 retry_after = parse_float_header(response, "Retry-After") rl_limit = parse_int_header(response, "X-RateLimit-Limit") rl_remaining = parse_int_header(response, "X-RateLimit-Remaining") rl_reset = parse_int_header(response, "X-RateLimit-Reset") if attempt >= max_retries raise RateLimitError.new( "AgentAdmit rate limit exceeded. Max retries (#{max_retries}) exhausted.", retry_after: retry_after, limit: rl_limit, remaining: rl_remaining, reset: rl_reset ) end # Compute wait: honor Retry-After header or use exponential backoff, cap at 30s wait_ms = retry_after ? (retry_after * 1000).ceil : [delay_ms, 30_000].min jitter_ms = rand(0..500) total_ms = wait_ms + jitter_ms warn "[AgentAdmit] Rate-limited (attempt #{attempt + 1}/#{max_retries}). " \ "Retrying in #{total_ms}ms." sleep(total_ms / 1000.0) delay_ms = [delay_ms * 2, 30_000].min next end # Non-429 response — process normally case status when 200 data = JSON.parse(response.body) # Check active flag (RFC 7662 introspection pattern). # The verify endpoint returns {active: false} with HTTP 200 for # invalid/expired/revoked tokens; the error code is one of # VERIFY_ERROR_CODES (e.g. token_expired, connection_expired, # environment_mismatch). Without this check, we'd read empty scopes. unless data["active"] reason = data["error"] || "invalid_token" raise InvalidTokenError.new("Token is not active: #{reason}", code: reason) end # insufficient_scope arrives with active: true (token valid, # requested scope not granted). if data["error"] == "insufficient_scope" raise InsufficientScopeError, data["error_description"] || "Scope not granted" end raise InvalidTokenError, "Introspection returned no user" if data["user_id"].nil? return IntrospectionResult.new( user_id: data["user_id"], connection_id: data["connection_id"], scopes: data["scopes"] || [], agent_label: data["agent_label"] || "Unknown Agent", sub: data["sub"], role: data["role"], app_id: data["app_id"], jti: data["jti"], exp: data["exp"] ) when 401 data = JSON.parse(response.body) rescue {} raise InvalidTokenError, data["error_description"] || "Token validation failed" else raise IntrospectionError, "Verification service returned #{response.code}" end end # Should never be reached raise IntrospectionError, "Unexpected exit from retry loop" end |