Skip to content

feat(metrics): mcp_auth_authorize_initiated_total funnel counter#27

Merged
babs merged 2 commits into
masterfrom
feat/authorize-initiated-metric
Apr 27, 2026
Merged

feat(metrics): mcp_auth_authorize_initiated_total funnel counter#27
babs merged 2 commits into
masterfrom
feat/authorize-initiated-metric

Conversation

@babs
Copy link
Copy Markdown
Owner

@babs babs commented Apr 27, 2026

Summary

Closes T4.3 from `misc/next-steps.md` — the last item in the Tier 1-2 backlog.

Adds the GET-side of the consent funnel. Operators can now answer:

  • "How many `/authorize` requests are entering the IdP fork?"
  • "Are clients abandoning the consent page?"
  • "What fraction of traffic takes the consent vs silent path?"

Where the increment fires

After every parameter validation (`client_id`, `redirect_uri`, `response_type`, `resource`, PKCE, `state`) and BEFORE the consent/silent fork. Pre-validation rejections — unknown `client_id`, unregistered `redirect_uri`, `response_type != "code"`, invalid `resource`, state-missing in strict mode — happen earlier and correctly skip the counter; those are surfaced via `access_denied_total{reason=...}` instead.

Funnel math

```
initiated → consent_decisions{approved|denied} → tokens_issued{authorization_code}
(consent fork)
↘ tokens_issued (silent fork — no consent step)
```

Derived rates:

  • `consent_rate = sum(consent_decisions) / authorize_initiated`
  • `silent_rate = 1 - consent_rate`
  • `consent_abandonment = 1 - (sum(consent_decisions) / authorize_initiated)` for the fraction of consent-fork users who initiated but never clicked Approve/Deny.

A spike in `initiated` without a matching spike downstream points at consent abandonment, callback failures (IdP-side, browser errors), replay rejections, or throttle 503s.

Implementation

  • Counter has no labels — minimal Prometheus cardinality. Per-fork breakdown is derivable from `consent_decisions` presence/absence (consent fork only fires there). A future operator who wants explicit per-fork rate can add a `path` label later — backwards-compatible since this is a brand-new counter.

Tests

Six tests across three semantic classes:

Class Test
Increments on consent fork `TestAuthorize_Initiated_IncrementsOnConsentFork`
Increments on silent fork `TestAuthorize_Initiated_IncrementsOnSilentFork`
Skips pre-funnel rejects `TestAuthorize_Initiated_NotIncrementedOnPreValidationReject` (table-driven: unknown_client_id / unsupported_response_type / invalid_resource / state_missing_strict)

The negative test structurally pins that the counter sits AFTER all validation. A future refactor that moves the increment above any reject path breaks at least one row.

Docs

`README.md` observability section gains an entry with the funnel-rate derivation formulas.

Test plan

  • CI green (`test`, `lint`, `keycloak-e2e`, `fuzz-smoke`, `manifest-prod`, `govulncheck`, `build`).
  • Manual: hit `/metrics` after one `/authorize` request and confirm `mcp_auth_authorize_initiated_total 1`.
  • No regression — pre-existing metrics unchanged.

babs added 2 commits April 28, 2026 01:25
Closes T4.3 from misc/next-steps.md. Adds the GET-side of the
consent funnel so operators can answer:

  - "How many /authorize requests are entering the IdP fork?"
  - "Are clients abandoning the consent page?"
  - "What fraction take the consent vs silent path?"

Increment is placed after every parameter validation (client_id,
redirect_uri, response_type, resource, PKCE, state) and BEFORE
the consent/silent fork. Pre-validation rejections — unknown
client_id, unregistered redirect_uri, response_type != "code",
invalid resource, state-missing in strict mode — happen earlier
and correctly skip the counter; they're surfaced via
access_denied_total{reason=...} instead.

Funnel math:

  initiated → consent_decisions{approved|denied} → tokens_issued
                  (consent fork)
              ↘ tokens_issued (silent fork — no consent step)

Derived rates: consent_rate = sum(consent_decisions) /
authorize_initiated; silent_rate = 1 - consent_rate.
A spike in initiated without a matching spike downstream points
at consent abandonment or callback failures (IdP-side, browser
errors, replay rejections, throttle 503s).

  - Counter has no labels — minimal Prometheus cardinality. A
    future operator who wants per-fork breakdown can derive it
    from consent_decisions presence/absence (consent fork only)
    or add a `path` label later (backwards-compatible).

  - Tests: 6 in total — 1 each for consent/silent fork + a
    table-driven test for the four pre-funnel reject classes
    (unknown client, unsupported response_type, invalid
    resource, state-missing strict). The negative test
    structurally pins that the counter sits AFTER all
    validation, so a future refactor that moves the increment
    line breaks at least one row.

  - README observability section updated with the funnel
    derivation formula.
Promote the funnel counter to a CounterVec with `path` label
(consent | silent) so operators can read per-fork rate
directly from PromQL without deriving it via the presence of
consent_decisions. Forensic signal added to the active-IdP-
session-phishing threat row: consent abandonment is the leading
indicator and now has an explicit metric formula.

  - metrics: AuthorizeInitiated → CounterVec, label `path`.

  - handlers/authorize.go: each fork increments its own label.
    Pre-validation rejections still skip the counter entirely.

  - tests: per-fork tests pin the right label moves AND the
    other does not (no cross-bleed). Pre-funnel-reject test
    asserts neither label moves.

  - README observability entry: PromQL fork-rate +
    consent-abandonment formulas using the labelled metric.

  - handlers/authorize.go comment reworded — increment lives
    inside each fork branch, not before the if statement.
@babs babs merged commit 4f33f36 into master Apr 27, 2026
7 checks passed
@babs babs deleted the feat/authorize-initiated-metric branch April 27, 2026 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant