keycardai-oauth

OAuth 2.0 primitives for the Keycard platform. The foundation gem of the Keycard Ruby SDK; keycardai-mcp and keycardai-a2a build on it.

Preview. APIs may change between minor versions while the surface settles. Conformance against the cross-SDK contract is tracked in the conformance report.

bundle add keycardai-oauth

Capabilities (per keycard-sdk-spec):

  • Token exchange and impersonation (RFC 8693)
  • Client credentials grant (RFC 6749 ยง4.4)
  • Authorization code + PKCE, including the challenge-driven loopback flow (RFC 8252)
  • Dynamic client registration (RFC 7591)
  • Authorization server discovery (RFC 8414)
  • JWT signing and verification, JWKS keyring with caching
  • Application credentials: ClientSecret (incl. multi-zone), WebIdentity (RFC 7523), WorkloadIdentity with pluggable identity token sources
  • AccessContext: the non-throwing per-request container for delegated tokens

Quickstart

Verify an inbound token

require "keycardai/oauth"

verifier = Keycardai::OAuth::TokenVerifier.new(
  issuers: "https://your-zone.keycard.cloud",
  audiences: "your-resource-id",
)

begin
  token = verifier.verify_token(bearer_token)
  puts token.subject
  puts token.scopes
rescue Keycardai::OAuth::InvalidTokenError => e
  # Fail closed: every verification failure is this one error type.
  warn "rejected: #{e.message}"
end

The verifier resolves signing keys through a JWKS keyring that caches per (issuer, kid), so a second verification of the same token does no network I/O.

Exchange a caller's token for a downstream resource

client = Keycardai::OAuth::TokenExchangeClient.new(
  issuer: "https://your-zone.keycard.cloud",
  client_id: ENV.fetch("KEYCARD_CLIENT_ID"),
  client_secret: ENV.fetch("KEYCARD_CLIENT_SECRET"),
)

result = client.exchange_token(
  subject_token: bearer_token,
  resource: "https://api.github.com",
  scope: "repo:read",
)
result.access_token

Nothing here reads the environment on your behalf. Configuration is read in application code and passed in explicitly, which is the cross-SDK contract.

Several resources at once

context = Keycardai::OAuth.exchange_tokens_for_resources(
  client: client,
  subject_token: bearer_token,
  resources: ["https://api.github.com", "https://slack.com/api"],
)

context.status                        # "success", "partial_error", or "error"
context.access("https://api.github.com")  # raises if that one resource failed
context.failed_resources

One resource failing never takes down the others; the failure lands on the context rather than raising, and access is where you choose to raise.

Act as a named user

result = client.impersonate(
  user_identifier: "user@example.com",
  resource: "https://api.github.com",
)

The issued token's sub is the target user. It is a leaf credential: a zone refuses it as the subject of a further exchange, so impersonate again rather than trying to trade it up.