# `Attesto.Introspection`
[🔗](https://github.com/XukuLLC/attesto/blob/v0.13.0/lib/attesto/introspection.ex#L1)

OAuth 2.0 Token Introspection (RFC 7662), conn-free core.

Given a presented token, decide whether it is currently active and, if so,
describe it with the RFC 7662 response members. This is the deliberate
introspection entry point; the transport layer authenticates the caller and
decides (by content negotiation) whether to return the plain JSON response or
a signed JWT (`Attesto.SignedIntrospection`, RFC 9701 / FAPI 2.0 Message
Signing §5.5). Nothing here touches a `conn`.

## What "active" means (RFC 7662 §2.2)

  * **Access tokens** are stateless JWTs: a token is active iff it passes the
    full access-token verification (`Attesto.Token.verify/3` - signature,
    issuer, audience, temporal, required claims, principal, and `typ ==
    "access"`), the same checks a resource server applies. The ONE check
    skipped is the sender-constraint **binding** (the proof-key match): the
    `cnf` is part of who may *use* the token, verified when it is presented,
    and the introspecting caller holds no proof key - so introspection passes
    `require_confirmation_binding: false`. The `cnf` *shape* is still
    validated, and the binding is echoed in the response so a resource server
    can check it (RFC 7662 / RFC 8705 §3.2 / RFC 9449).

  * **Refresh tokens** are opaque, stored secrets: a refresh token present in
    the store, unconsumed, and unexpired is reported active, a consumed
    (rotated) or expired one inactive. The stored record's own data contract
    (`Attesto.RefreshToken` build context) carries the subject, granted scope,
    presenting client, and optional DPoP binding, so those RFC 7662 members
    are surfaced when present (`sub`/`scope`/`client_id`/`cnf`) - for a
    resource server, and so an `:authorize` policy can decide per token. A
    store that does not populate them yields the minimal `active`+`exp`
    response.

  * Anything else - a malformed token, a forged signature, an expired token,
    or one absent from the store - is reported inactive (`%{"active" => false}`),
    never an error: RFC 7662 §2.2 forbids a token-existence oracle.

`token_type_hint` (RFC 7662 §2.1) only reorders which check is tried first; an
unmatched hint still falls through to the other.

# `opts`

```elixir
@type opts() :: [
  refresh_store: module() | nil,
  token_type_hint: String.t() | nil,
  authorize: (response() -&gt; boolean()) | nil,
  now: integer() | DateTime.t()
]
```

# `response`

```elixir
@type response() :: %{required(String.t()) =&gt; term()}
```

# `introspect`

```elixir
@spec introspect(Attesto.Config.t(), String.t(), opts()) :: response()
```

Introspect `token`, returning the RFC 7662 response map (always `active`,
plus the token's members when active). Never returns an error.

Options:

  * `:refresh_store` - an `Attesto.RefreshStore` module to consult for opaque
    refresh tokens; when absent, only access tokens are introspected.
  * `:token_type_hint` - `"access_token"` or `"refresh_token"` (RFC 7662
    §2.1); reorders the attempts, never restricts them.
  * `:authorize` - a 1-arity predicate `(response -> boolean)` consulted with
    the active response *before* it is returned (RFC 7662 §4 / RFC 9701 §5:
    the AS MAY restrict which tokens a caller may introspect). The transport
    layer captures the authenticated caller identity in this closure; if it
    does not return `true` - or it raises - the response is downgraded to
    `%{"active" => false}` so a caller not authorized for the token learns
    nothing about it (FAPI: a regular client querying introspection is a
    leakage risk). When absent, every authenticated caller may introspect any
    token (the single-trust-domain default).
  * `:now` - the reference time (Unix seconds or `DateTime`), for tests.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
