Class: Keycardai::OAuth::TokenExchangeClient

Inherits:
Object
  • Object
show all
Includes:
TokenRequests
Defined in:
lib/keycardai/oauth/token_exchange_client.rb

Overview

RFC 8693 token exchange: swap a subject token for a fresh token scoped to a downstream resource, the core delegation primitive. Also exposes the impersonation convenience, a substitute-user exchange where the authorization server derives the acting party from client authentication.

The token endpoint is discovered from the issuer on first use and cached. Requests do not retry transparently.

Instance Method Summary collapse

Methods included from TokenRequests

error_for, parse_response

Constructor Details

#initialize(issuer:, credential: nil, client_id: nil, client_secret: nil, http_client: HTTP::NetHTTPClient.new, timeout: nil) ⇒ TokenExchangeClient

Returns a new instance of TokenExchangeClient.

Parameters:

  • issuer (String)

    the zone's issuer URL

  • credential (Object, nil) (defaults to: nil)

    an application credential (ClientSecret, WebIdentity, WorkloadIdentity); exclusive with client_id/client_secret

  • client_id (String, nil) (defaults to: nil)

    shared-secret client id (HTTP Basic)

  • client_secret (String, nil) (defaults to: nil)

    shared-secret client secret; provide both or neither

  • http_client (#get, #post_form) (defaults to: HTTP::NetHTTPClient.new)

    pluggable transport

  • timeout (Numeric, nil) (defaults to: nil)

    request timeout in seconds

Raises:

  • (ConfigurationError)

    when only one of client_id/client_secret is given, or a credential is combined with a raw pair



26
27
28
29
30
# File 'lib/keycardai/oauth/token_exchange_client.rb', line 26

def initialize(issuer:, credential: nil, client_id: nil, client_secret: nil,
               http_client: HTTP::NetHTTPClient.new, timeout: nil)
  initialize_token_client(issuer: issuer, credential: credential, client_id: client_id,
                          client_secret: client_secret, http_client: http_client, timeout: timeout)
end

Instance Method Details

#exchange_token(subject_token:, subject_token_type: nil, resource: nil, audience: nil, scope: nil, requested_token_type: nil, actor_token: nil, actor_token_type: nil, client_assertion: nil, client_assertion_type: nil, issuer: nil) ⇒ TokenResponse

Exchange a subject token (RFC 8693).

Parameters:

  • subject_token (String)

    the token being exchanged

  • subject_token_type (String) (defaults to: nil)

    type of the subject token

  • resource (String, nil) (defaults to: nil)

    RFC 8707 target resource

  • audience (String, nil) (defaults to: nil)

    target audience (alternative to resource)

  • scope (String, nil) (defaults to: nil)

    space-separated scopes for the issued token

  • requested_token_type (String, nil) (defaults to: nil)
  • actor_token (String, nil) (defaults to: nil)

    the acting party's token, for explicit delegation; requires actor_token_type

  • actor_token_type (String, nil) (defaults to: nil)
  • client_assertion (String, nil) (defaults to: nil)

    client-authentication assertion, form-encoded in the body

  • client_assertion_type (String, nil) (defaults to: nil)
  • issuer (String, nil) (defaults to: nil)

    per-call zone selection: resolves the zone's token endpoint and, for a multi-zone credential, that zone's client authentication. Defaults to the client's issuer.

Returns:

Raises:



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/keycardai/oauth/token_exchange_client.rb', line 53

def exchange_token(subject_token:, subject_token_type: nil, resource: nil,
                   audience: nil, scope: nil, requested_token_type: nil, actor_token: nil,
                   actor_token_type: nil, client_assertion: nil, client_assertion_type: nil,
                   issuer: nil)
  raise ArgumentError, "actor_token_type is required when actor_token is set" if actor_token && !actor_token_type

  target = issuer || @issuer
  overrides = {
    "subject_token_type" => subject_token_type,
    "resource" => resource,
    "audience" => audience,
    "scope" => scope,
    "requested_token_type" => requested_token_type,
    "actor_token" => actor_token,
    "actor_token_type" => actor_token_type,
    "client_assertion" => client_assertion,
    "client_assertion_type" => client_assertion_type
  }.compact
  post_token_request(base_exchange_params(subject_token, target).merge(overrides), issuer: target)
end

#impersonate(user_identifier:, resource:, scope: nil, issuer: nil) ⇒ TokenResponse

Impersonate a named user: a substitute-user token exchange for privileged operations performed on the user's behalf. No actor token is sent; the authorization server derives the acting party from client authentication and records it in the issued token's act chain.

Parameters:

  • user_identifier (String)

    the target user (becomes sub)

  • resource (String)

    target resource for the issued token

  • scope (String, nil) (defaults to: nil)

    space-separated scopes

  • issuer (String, nil) (defaults to: nil)

    per-call zone selection

Returns:

Raises:

  • (OAuthError)

    invalid_grant (unknown user), unauthorized_client (impersonation not permitted), and all token-exchange errors



86
87
88
89
90
91
92
93
94
95
96
# File 'lib/keycardai/oauth/token_exchange_client.rb', line 86

def impersonate(user_identifier:, resource:, scope: nil, issuer: nil)
  raise ArgumentError, "resource must be a non-empty string" if resource.nil? || resource.empty?

  exchange_token(
    subject_token: OAuth.build_substitute_user_token(user_identifier),
    subject_token_type: TokenType::SUBSTITUTE_USER,
    resource: resource,
    scope: scope,
    issuer: issuer
  )
end