1-SEC contributors · Reviewed · Module: token_vault

Agent Token Exchange: Audit Counters, Failure Signals and Scope Changes

The optional Token Vault component records exchange requests and aggregate connection counters.

What 1-SEC checks

The optional Token Vault component records exchange requests and aggregate connection counters. Its direct Auth0 exchange client updates success/failure counters for its own operations. Reported response events with success equal to the string false or a nonempty error are logged; they do not update those counters or independently emit a failure alert. Scope monitoring establishes a first-observed set per agent and connection, then alerts when later requests introduce unseen scope strings. Newly observed scopes become part of that local set.

Read the implementation: handleTokenExchange

Required telemetry and context

Relevant input events: token_exchange, token_exchange_request, token_exchange_response. Use a trusted producer at the real application boundary.

  • connection
  • agent_id
  • subject_token_type
  • scopes (space-separated string)
  • success (string on response events)
  • error

Field names above describe relevant context, not a complete API request. Consult the current event contract for envelope and detail requirements. Preserve booleans, numbers, and strings as the receiving handler expects them. Agent-provided text cannot independently verify identity or authorization.

Deployment, prevention, and response

Enable Token Vault deliberately and configure the actual identity-provider integration before active exchange. Send trusted agent/connection context and preserve provider decisions in your own audit system. Treat scope-change alerts as investigation signals and apply least privilege at the identity provider and resource server. Distinguish direct-client failures from reported failure telemetry when interpreting counters. Never place subject tokens, refresh tokens or client secrets in example events, URLs or retained diagnostics.

Record the source event, affected agent/host, actual target, policy decision, and response result. Review enforcement configuration separately from detection. A recommendation, selected action, dry-run record, and completed action are different outcomes.

Reproduce the source fixtures

From a checkout of the linked 1-SEC source revision, run the selected fixtures with the repository's Go toolchain. These commands run tests; they do not execute a real attack against your systems.

go test ./internal/modules/tokenvault -run '^(TestExchangeTracker|TestScopeEscalation|TestTokenExchangeConstants|TestEventTypes)$' -count=1

The fixtures test aggregate request/success/failure counters, scope-baseline behavior and unchanged/new-scope controls, constants and subscribed event types. They do not call Auth0, exercise provider errors or validate an audience, rate-limiting integration or durable audit store.

TestExchangeTracker

Full fixture and assertions — run in its original package with its helpers.

func TestExchangeTracker(t *testing.T) {
	et := NewExchangeTracker()

	id1 := et.RecordRequest("google-oauth2", SubjectTokenTypeRefreshToken)
	et.RecordSuccess(id1, "openid profile email")

	id2 := et.RecordRequest("github", SubjectTokenTypeAccessToken)
	et.RecordFailure(id2, "invalid_grant")

	stats := et.Stats()
	if stats.TotalExchanges != 2 {
		t.Errorf("expected 2 total exchanges, got %d", stats.TotalExchanges)
	}
	if stats.SuccessCount != 1 {
		t.Errorf("expected 1 success, got %d", stats.SuccessCount)
	}
	if stats.FailureCount != 1 {
		t.Errorf("expected 1 failure, got %d", stats.FailureCount)
	}
	if stats.ByConnection["google-oauth2"] != 1 {
		t.Errorf("expected 1 google-oauth2 exchange, got %d", stats.ByConnection["google-oauth2"])
	}
}
TestScopeEscalation

Full fixture and assertions — run in its original package with its helpers.

func TestScopeEscalation(t *testing.T) {
	et := NewExchangeTracker()

	// First request — baseline
	esc := et.DetectScopeEscalation("agent-1", "google-oauth2", "openid profile")
	if esc != "" {
		t.Errorf("expected no escalation for first request, got %q", esc)
	}

	// Same scopes — no escalation
	esc = et.DetectScopeEscalation("agent-1", "google-oauth2", "openid profile")
	if esc != "" {
		t.Errorf("expected no escalation for same scopes, got %q", esc)
	}

	// New scope — escalation
	esc = et.DetectScopeEscalation("agent-1", "google-oauth2", "openid profile email calendar")
	if esc == "" {
		t.Error("expected escalation for new scopes")
	}
	if esc != "email, calendar" {
		t.Errorf("expected escalation %q, got %q", "email, calendar", esc)
	}
}
TestTokenExchangeConstants

Full fixture and assertions — run in its original package with its helpers.

func TestTokenExchangeConstants(t *testing.T) {
	// Verify constants match Auth0 official SDK values
	if GrantTypeFederatedConnectionAccessToken != "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token" {
		t.Error("grant type does not match Auth0 SDK")
	}
	if SubjectTokenTypeRefreshToken != "urn:ietf:params:oauth:token-type:refresh_token" {
		t.Error("refresh token type does not match RFC 8693")
	}
	if SubjectTokenTypeAccessToken != "urn:ietf:params:oauth:token-type:access_token" {
		t.Error("access token type does not match RFC 8693")
	}
	if RequestedTokenTypeFederatedConnection != "http://auth0.com/oauth/token-type/federated-connection-access-token" {
		t.Error("requested token type does not match Auth0 SDK")
	}
}
TestEventTypes

Full fixture and assertions — run in its original package with its helpers.

func TestEventTypes(t *testing.T) {
	types := EventTypes()
	if len(types) == 0 {
		t.Fatal("expected non-empty event types")
	}

	required := []string{
		"token_exchange", "connected_account_link", "connected_account_unlink",
		"oauth_grant",
	}
	typeSet := make(map[string]bool, len(types))
	for _, et := range types {
		typeSet[et] = true
	}
	for _, r := range required {
		if !typeSet[r] {
			t.Errorf("missing required event type: %s", r)
		}
	}
}

Tuning, evaluation, and coverage limits

Observed scope history is not an approved grant policy or proof that the provider granted those scopes. The first request establishes a baseline without escalation alerting. The recent-exchange helper has agent timestamps in a separate map, but the normal request-recording path does not populate that map, so do not rely on the advertised per-agent rate threshold. Aggregate counters and a bounded in-memory request list are not an immutable per-exchange audit ledger. There is no demonstrated audience validation or sender-constraining in these event handlers. Helper fixtures do not prove a live Auth0 exchange works.

Evaluate this behavior with representative authorized activity and a controlled known-positive case. Investigate the exact rule, inputs, and state before suppressing an alert. Do not assume a hard-coded heuristic is a configurable setting. Measure your own deployment; the source fixtures do not establish a benchmark, detection rate, compliance certification, or incident-prevention claim.

Protocol and format references

These references describe the underlying format or protocol. The implementation and fixtures above define the subset 1-SEC actually checks.

Related implemented controls

Implementation reviewed at source revision a2ab27289e0fbbce555ad80f31eb19bfffa86f84. Recheck the actual release and integration before deployment. Sources and corrections policy.