{"id":46094874,"url":"https://github.com/hack-ink/oauth2-broker","last_synced_at":"2026-03-01T18:31:54.549Z","repository":{"id":324672098,"uuid":"1097955364","full_name":"hack-ink/oauth2-broker","owner":"hack-ink","description":"Rust’s turnkey OAuth 2.0 broker—spin up multi-tenant flows, CAS-smart token stores, and transport-aware observability in one crate built for production.","archived":false,"fork":false,"pushed_at":"2025-12-10T09:40:12.000Z","size":213,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-10T12:48:37.424Z","etag":null,"topics":["async","broker","oauth2","reqwest","token"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/oauth2-broker","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hack-ink.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-11-17T04:20:43.000Z","updated_at":"2025-12-10T09:40:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hack-ink/oauth2-broker","commit_stats":null,"previous_names":["hack-ink/oauth2-broker"],"tags_count":4,"template":false,"template_full_name":"hack-ink/rust-initializer","purl":"pkg:github/hack-ink/oauth2-broker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hack-ink%2Foauth2-broker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hack-ink%2Foauth2-broker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hack-ink%2Foauth2-broker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hack-ink%2Foauth2-broker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hack-ink","download_url":"https://codeload.github.com/hack-ink/oauth2-broker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hack-ink%2Foauth2-broker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29978530,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-01T16:35:47.903Z","status":"ssl_error","status_checked_at":"2026-03-01T16:35:44.899Z","response_time":124,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","broker","oauth2","reqwest","token"],"created_at":"2026-03-01T18:31:53.924Z","updated_at":"2026-03-01T18:31:54.540Z","avatar_url":"https://github.com/hack-ink.png","language":"Rust","readme":"\u003cdiv align=\"center\"\u003e\n\n# oauth2-broker\n\nRust’s turnkey OAuth 2.0 broker—spin up multi-tenant flows, CAS-smart token stores, and transport-aware observability in one crate built for production.\n\n[![License](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)\n[![Docs](https://img.shields.io/docsrs/oauth2-broker)](https://docs.rs/oauth2-broker)\n[![Rust](https://github.com/hack-ink/oauth2-broker/actions/workflows/rust.yml/badge.svg?branch=main)](https://github.com/hack-ink/oauth2-broker/actions/workflows/rust.yml)\n[![Release](https://github.com/hack-ink/oauth2-broker/actions/workflows/release.yml/badge.svg)](https://github.com/hack-ink/oauth2-broker/actions/workflows/release.yml)\n[![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/hack-ink/oauth2-broker)](https://github.com/hack-ink/oauth2-broker/tags)\n[![GitHub last commit](https://img.shields.io/github/last-commit/hack-ink/oauth2-broker?color=red\u0026style=plastic)](https://github.com/hack-ink/oauth2-broker)\n[![GitHub code lines](https://tokei.rs/b1/github/hack-ink/oauth2-broker)](https://github.com/hack-ink/oauth2-broker)\n\n\u003c/div\u003e\n\n## Table of Contents\n\n- [Why oauth2-broker?](#why-oauth2-broker)\n- [Overview](#overview)\n- [Quickstart](#quickstart)\n- [Module Layout](#module-layout)\n- [Broker Capabilities](#broker-capabilities)\n- [Custom HTTP Transports](#custom-http-transports)\n- [Feature Flags](#feature-flags)\n- [Extension Traits](#extension-traits)\n- [Observability](#observability)\n- [Examples \u0026 Further Reading](#examples--further-reading)\n- [Development Guardrails](#development-guardrails)\n- [Support Me](#support-me)\n- [Appreciation](#appreciation)\n- [Additional Acknowledgements](#additional-acknowledgements)\n\n## Why oauth2-broker?\n\n- **Industry-grade OAuth 2.0 broker for Rust** — The ecosystem lacks a turnkey, multi-tenant token\n  broker, forcing teams to rebuild authorization code, refresh, and service-to-service flows from\n  scratch. This crate fills that gap with the explicit goal of delivering an industry-level control\n  plane for OAuth clients.\n- **Flow orchestration baked in** — `Broker::start_authorization`, `Broker::exchange_code`,\n  `Broker::refresh_access_token`, and `Broker::client_credentials` already coordinate state, PKCE,\n  caching, and persistence so product code can focus on user experience instead of grant semantics.\n- **Deterministic storage + concurrency** — The `BrokerStore` trait, `MemoryStore`, `FileStore`,\n  singleflight guard helpers, and `CachedTokenRequest` windows keep token records consistent while\n  letting downstream crates plug in Redis, SQL, or bespoke backends without touching flow logic.\n- **Pluggable HTTP + error mapping** — `TokenHttpClient`, `ReqwestHttpClient`,\n  `Broker::with_http_client`, and `ResponseMetadataSlot` isolate transport wiring while\n  `TransportErrorMapper` keeps error classification observable and stack-agnostic.\n- **Operational visibility by default** — `FlowSpan`, `FlowOutcome`, `RefreshMetrics`, and\n  provider-aware descriptors emit structured traces, counters, and tenant/provider labels so SREs\n  can enforce budgets and SLAs without bolting on custom instrumentation.\n\n## Overview\n\n`oauth2-broker` exposes a `Broker\u003cC, M\u003e` facade that coordinates OAuth 2.0 flows for a single\n`ProviderDescriptor`. Each broker instance owns the token store (`BrokerStore`), provider strategy,\nHTTP client, and transport error mapper, so callers inject tenant/principal/scope context while the\ncrate reuses shared connection pools and descriptor metadata. Under the hood the crate drives the\nupstream `oauth2` client through the `BasicFacade`, layering caching, concurrency control, and\nobservability on top.\n\nThe current codebase ships production-ready implementations for the flows already wired inside\n`src/flows/`:\n\n- `Broker::start_authorization` and `Broker::exchange_code` wrap Authorization Code + PKCE,\n  generating state, PKCE pairs, and storing `TokenRecord` values after exchanging the returned\n  `code`.\n- `Broker::refresh_access_token` rotates refresh secrets via `BrokerStore::compare_and_swap_refresh`,\n  records metrics through `RefreshMetrics`, and removes revoked tokens when providers return\n  `invalid_grant`.\n- `Broker::client_credentials` reuses cached service-to-service tokens, enforces jittered expiry\n  windows via `CachedTokenRequest`, and uses per-`StoreKey` singleflight guards so concurrent\n  callers ride the same refresh.\n\nCaching and persistence are abstracted behind the `BrokerStore` trait, with in-tree backends for an\nin-memory `MemoryStore` and a JSON-backed `FileStore`. Stores expose compare-and-swap refresh\nsemantics, revocation helpers, and a stable `StoreKey` fingerprint so downstream crates can add\nRedis or SQL implementations without touching the flow code.\n\nHTTP behavior is centralized in `TokenHttpClient` and `TransportErrorMapper`. The crate ships\n`ReqwestHttpClient` plus `ReqwestTransportErrorMapper`, and any caller can replace them via\n`Broker::with_http_client` to reuse custom TLS, retry, or proxy stacks. Each token request carries a\n`ResponseMetadataSlot`, giving error mappers access to HTTP status codes and `Retry-After` hints\nwhen translating transport failures.\n\nObservability hooks live under `obs/`: every flow emits `FlowSpan` traces and success/attempt/failure\ncounters, while refresh operations also increment `RefreshMetrics`. Provider quirks and client-auth\nrules are modeled by `ProviderDescriptor`, `GrantType`, and `ProviderStrategy`, so higher-level\nsystems configure descriptor data and then let the broker enforce those rules consistently across\nevery flow.\n\n## Quickstart\n\n```rust\nuse color_eyre::Result;\nuse oauth2_broker::{\n\tauth::{PrincipalId, ProviderId, ScopeSet, TenantId},\n\tflows::{Broker, CachedTokenRequest},\n\tprovider::{DefaultProviderStrategy, GrantType, ProviderDescriptor, ProviderStrategy},\n\tstore::{BrokerStore, MemoryStore},\n};\nuse std::sync::Arc;\nuse url::Url;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c()\u003e {\n\tcolor_eyre::install()?;\n\n\tlet store: Arc\u003cdyn BrokerStore\u003e = Arc::new(MemoryStore::default());\n\tlet strategy: Arc\u003cdyn ProviderStrategy\u003e = Arc::new(DefaultProviderStrategy);\n\n\tlet descriptor = ProviderDescriptor::builder(ProviderId::new(\"demo-provider\")?)\n\t\t.authorization_endpoint(Url::parse(\"https://provider.example.com/authorize\")?)\n\t\t.token_endpoint(Url::parse(\"https://provider.example.com/token\")?)\n\t\t.support_grants([\n\t\t\tGrantType::AuthorizationCode,\n\t\t\tGrantType::RefreshToken,\n\t\t\tGrantType::ClientCredentials,\n\t\t])\n\t\t.build()?;\n\n\tlet broker = Broker::new(store, descriptor, strategy, \"demo-client\")\n\t\t.with_client_secret(\"demo-secret\");\n\n\tlet scope = ScopeSet::new([\"email.read\", \"profile.read\"])?;\n\tlet request = CachedTokenRequest::new(\n\t\tTenantId::new(\"tenant-acme\")?,\n\t\tPrincipalId::new(\"svc-router\")?,\n\t\tscope,\n\t);\n\n\tlet record = broker.client_credentials(request).await?;\n\tprintln!(\"access token: {}\", record.access_token.expose());\n\tOk(())\n}\n```\n\nThe snippet relies on the broker's default reqwest-backed transport, the in-memory store, and the\nzero-cost `DefaultProviderStrategy` to reuse cached service-to-service tokens with the\n`client_credentials` grant. For a mock-backed walkthrough that spins up an in-process\n`httpmock` server, see [`examples/client_credentials.rs`](examples/client_credentials.rs);\nan authorization-code state/PKCE walk-through lives in\n[`examples/start_authorization.rs`](examples/start_authorization.rs).\nA provider-specific Authorization Code + PKCE setup for X (Twitter) is available in\n[`examples/x_authorization.rs`](examples/x_authorization.rs). It prints the X authorize URL,\nprompts for the returned `state` and `code` via stdin, exchanges them, and can post a\ntweet when you run `cargo make example-x-authorization` with real client credentials.\n\n## Module Layout\n\n- `src/flows/common.rs` centralizes scope formatting, token-response parsing, HTTP error mapping,\n  and singleflight guard lookups. Flow-specific directories keep their heavy logic contained:\n  `auth_code_pkce/session.rs` owns PKCE/session structs, `client_credentials/request.rs` holds the\n  jittered cache request type, and `refresh/{request,metrics}.rs` split refresh inputs from the\n  counter set shared with `Broker`.\n- `src/provider/descriptor/` now mirrors the descriptor structure itself—`grant.rs` defines\n  `GrantType`/`SupportedGrants`, `quirks.rs` captures `ProviderQuirks`, and `builder.rs` handles the\n  builder plus validation. Customized HTTP behavior lives with `Broker::with_http_client`, so tests\n  and downstream crates can inject any `TokenHttpClient` implementation without env-variable shims.\n- `src/types/token/` separates concerns across `secret.rs`, `family.rs`, and `record.rs`, keeping the\n  redacted secret wrapper isolated from the lifecycle-heavy record/builder logic.\n- `src/obs/metrics.rs` and `src/obs/tracing.rs` keep feature-flagged observability hooks small so\n  `obs/mod.rs` remains a thin façade.\n\n## Broker Capabilities\n\n### OAuth 2.0 flows (MVP)\n\n- **Authorization Code + PKCE** — `Broker::start_authorization` generates state + PKCE material,\n  with `Broker::exchange_code` handling HTTPS token exchanges, descriptor-driven PKCE\n  enforcement, and store persistence.\n- **Refresh Token** — `Broker::refresh_access_token` enforces singleflight guards per\n  tenant/principal/scope tuple, rotates refresh tokens through the store’s CAS helpers, and\n  surfaces telemetry via `RefreshMetrics`.\n- **Client Credentials** — `Broker::client_credentials` reuses cached app-only tokens, joins\n  scopes per provider delimiter, and re-enters the provider only when forced or nearing expiry.\n\n### Storage \u0026 caching\n\n- Public `BrokerStore` trait defines `fetch`, `save`, `revoke`, and refresh CAS semantics.\n- `MemoryStore` (thread-safe) is the default backend for tests/examples; downstream integrators\n  can implement `BrokerStore` for Redis, SQL, etc. without touching flows.\n\n### HTTP handling\n\n- Every broker owns a dedicated `TokenHttpClient` handle (`ReqwestHttpClient` by default), so\n  downstream code never wires transports or toggles HTTP-specific feature flags unless they opt in.\n- `Broker::with_http_client` accepts any type that implements `TokenHttpClient` plus a\n  corresponding `TransportErrorMapper`, making it easy to reuse custom TLS, proxy, timeout, or\n  entirely different HTTP stacks whenever the default transport is not sufficient. The same generic\n  pair drives the internal `BasicFacade`, so every flow consistently works with custom transports.\n- Token requests are constructed internally from descriptors, grant types, and strategies, keeping\n  the public API focused on OAuth concepts instead of HTTP primitives.\n- The default `reqwest` feature provisions the transport automatically so Quickstart snippets stay\n  zero-config, but you can disable it when wiring a custom `TokenHttpClient`.\n\n### Extension traits\n\n- `RequestSignerExt` — describe how to attach broker-issued tokens to downstream HTTP clients.\n- `TokenLeaseExt` — model short-lived access to cached records with readiness metadata.\n- `RateLimitPolicy` — consult tenant/provider budgets and return `Allow`, `Delay`, or retry hints\n  before flows hit upstream token endpoints.\n\n### Observability \u0026 instrumentation\n\n- Feature flag `tracing` emits `oauth2_broker.flow` spans for `authorization_code`, `refresh`,\n  and `client_credentials` stages without leaking secrets.\n- Feature flag `metrics` increments `oauth2_broker_flow_total` counters (labels: `flow`,\n  `outcome`) so exporters such as Prometheus can track attempts/success/failure rates.\n- Flows call into the observation helpers directly so downstream crates only need to opt into the\n  features and provide their preferred subscriber/recorder configuration.\n\n## Feature Flags\n\n- `reqwest` *(default)* — Enables the bundled reqwest transport, `Broker::new`, integration test\n  helpers, and reqwest-based examples. Disable it (`--no-default-features` or\n  `default-features = false`) when you supply your own `TokenHttpClient` and mapper via\n  `Broker::with_http_client`.\n- `test` — Re-exports the `_preludet` helpers outside of `cfg(test)` so downstream crates can reuse\n  the integration harness.\n\n## Custom HTTP Transports\n\n### Default transport\n\n`Broker\u003cC, M\u003e` and the internal `BasicFacade\u003cC, M\u003e` are generic over both the transport and the\nmapper. Calling `Broker::new` (when the `reqwest` feature is enabled) instantiates those generics as\n`Broker\u003cReqwestHttpClient, ReqwestTransportErrorMapper\u003e`, which keeps the Quickstart and HTTP-backed\nexamples zero-config. `TokenHttpClient`, `ResponseMetadata`, `ResponseMetadataSlot`, and\n`TransportErrorMapper` are re-exported from the crate root so downstream crates can wire their own\nstack without depending on private modules.\n\n### Registering a custom transport\n\nWhen you need to wrap an alternate pool, TLS stack, or test double, call\n`Broker::with_http_client(store, descriptor, strategy, client_id, my_client, my_mapper)` and follow\nthese steps:\n\n1. Implement `TokenHttpClient` for your transport. The `Handle` type you expose must implement\n   `oauth2::AsyncHttpClient` and stay `Send + 'static`. The associated `TransportError` can be any\n   `Send + Sync + 'static` value, so your stack never has to reference `reqwest::Error`.\n2. Emit `ResponseMetadata` by cloning the provided `ResponseMetadataSlot`, calling `slot.take()`\n   before dispatching the request, and persisting status plus `Retry-After` via `slot.store(...)` as\n   soon as headers arrive.\n3. Implement `TransportErrorMapper\u003cTransportError\u003e` so the broker can translate\n   `HttpClientError\u003cTransportError\u003e` plus metadata into its `Error` classification.\n4. Wrap both handles in `Arc` (the broker clones them internally) and pass them to\n   `Broker::with_http_client` alongside your descriptor, provider strategy, and OAuth client ID.\n\n`examples/custom_transport.rs` contains a complete walkthrough that registers a mock transport with\na bespoke error type while keeping metadata and mapper wiring intact.\n\n### TokenHttpClient contract\n\n`TokenHttpClient` hands `oauth2` an `AsyncHttpClient` handle that owns a clone of a\n`ResponseMetadataSlot`, ensuring every transport stores the final HTTP status and `Retry-After`\nhints in a [`ResponseMetadata`] value. Implementations must:\n\n1. Call `slot.take()` before dispatching the request so stale metadata never leaks between retries.\n2. Populate `ResponseMetadata` via `slot.store(...)` as soon as the status/headers are available.\n3. Return an `AsyncHttpClient` handle whose future is `Send + 'static` so broker flows can box it.\n4. Propagate the associated `TransportError` type through `AsyncHttpClient::Error`.\n\nThe `ResponseMetadataSlot` and `ResponseMetadata` types are re-exported from the crate root, which\nmakes it easy to satisfy the contract without digging through internal modules.\n\n### Mapper requirements\n\nWhenever a transport emits `HttpClientError\u003cE\u003e`, the mapper receives the provider strategy, active\ngrant, and the freshest metadata. The trait signature is intentionally public so you can depend on\nit directly:\n\n```rust\npub trait TransportErrorMapper\u003cE\u003e: Send + Sync + 'static {\n\tfn map_transport_error(\n\t\t\u0026self,\n\t\tstrategy: \u0026dyn ProviderStrategy,\n\t\tgrant: GrantType,\n\t\tmetadata: Option\u003c\u0026ResponseMetadata\u003e,\n\t\terror: HttpClientError\u003cE\u003e,\n\t) -\u003e oauth2_broker::error::Error;\n}\n```\n\nUse this callback to translate transport-specific errors into `TransientError`, `TransportError`,\nor any other variant that callers rely on for retry/backoff logic. In practice, mappers should:\n\n- Inspect `ResponseMetadata` for HTTP status and `Retry-After` hints before picking a retry class.\n- Treat `HttpClientError::Reqwest(inner)` as \"transport error\" even if `inner` is your custom\n  `TransportError`. The upstream `oauth2` crate kept the variant name for compatibility while the\n  payload type is now generic.\n- Fall back to `HttpClientError::Other`, `HttpClientError::Http`, and `HttpClientError::Io` to\n  retain error context that does not come from the transport.\n\n`ReqwestTransportErrorMapper` demonstrates how reqwest errors become broker `Error` values, and the\ncustom transport example mirrors the exact pattern for a mock error type.\n\n### Registering the client and mapper\n\nBoth the client and mapper typically live behind `Arc` handles so every broker instance can share\nthem:\n\n```rust\nlet http_client = Arc::new(MockHttpClient::default());\nlet mapper = Arc::new(MockTransportErrorMapper::default());\nlet broker = Broker::with_http_client(store, descriptor, strategy, \"demo-client\", http_client, mapper)\n    .with_client_secret(\"demo-secret\");\n```\n\nThe [`examples/custom_transport.rs`](examples/custom_transport.rs) walkthrough demonstrates a mock\ntransport with a non-reqwest error type, ensures metadata recording still works, and wires a mapper\nthat forwards those errors to the broker. Use it as a template whenever you need to plug in a\ncustom HTTP stack, simulator, or integration-test fake.\n\n## Feature Flags\n\n| Feature   | Default | Description                                                                                             |\n| --------- | ------- | ------------------------------------------------------------------------------------------------------- |\n| `tracing` | ❌      | Emits `tracing` spans named `oauth2_broker.flow` so downstream apps can correlate grant attempts.       |\n| `metrics` | ❌      | Increments the `oauth2_broker_flow_total` counter via the `metrics` crate with `flow`/`outcome` labels. |\n\n## Extension Traits\n\nThe MVP ships **contracts only** for higher-level integrations so downstream crates can\nexperiment without waiting on broker-owned implementations:\n\n- `ext::RequestSignerExt\u003cRequest, Error\u003e` — describes how to attach broker-issued tokens to any\n  request builder (the docs show a `reqwest::RequestBuilder` example).\n- `ext::TokenLeaseExt\u003cLease, Error\u003e` — models short-lived access to a `TokenRecord` via\n  lease/guard types along with supporting metadata (`TokenLeaseContext`, `TokenLeaseState`).\n- `ext::RateLimitPolicy\u003cError\u003e` — lets flows consult tenant/provider rate budgets before hitting\n  providers using `RateLimitContext`, `RateLimitDecision`, and `RetryDirective` helpers.\n\nAll three traits live under `src/ext/`, include doc-tested examples, and intentionally ship **no\ndefault implementations** in this MVP so consumers can plug their own HTTP stack, token cache, and\nrate-limit store without extra dependencies.\n\n## Observability\n\nTracing + metrics ship **disabled by default** so downstream crates only pay for what they enable.\nTurn them on explicitly in `Cargo.toml`:\n\n```toml\n[dependencies]\noauth2-broker = { version = \"0.0.1\", features = [\"tracing\", \"metrics\"] }\n```\n\n- `tracing` creates spans named `oauth2_broker.flow` with `flow` (`authorization_code`,\n  `refresh`, or `client_credentials`) and `stage` (`start_authorization`, `exchange_code`, etc.).\n  Only enum labels are recorded so client IDs, secrets, and tokens never leave the crate. You can\n  also open spans in your own adapters using the helpers:\n\n    ```rust\n    #[cfg(feature = \"tracing\")]\n    {\n    \tuse oauth2_broker::obs::{FlowKind, FlowSpan};\n    \tlet _guard = FlowSpan::new(FlowKind::AuthorizationCode, \"my_adapter\").entered();\n    }\n    ```\n\n- `metrics` increments a counter named `oauth2_broker_flow_total` via the `metrics` crate every\n  time a flow attempts, succeeds, or fails. Labels mirror the tracing fields so exporters like\n  Prometheus or OpenTelemetry can break down rates per grant/outcome:\n\n    ```rust\n    #[cfg(feature = \"metrics\")]\n    {\n    \tuse oauth2_broker::obs::{record_flow_outcome, FlowKind, FlowOutcome};\n    \trecord_flow_outcome(FlowKind::ClientCredentials, FlowOutcome::Attempt);\n    }\n    ```\n\nSet up your preferred `tracing` subscriber and `metrics` recorder (for example,\n`metrics-exporter-prometheus`) to collect the emitted data.\n\n## Examples \u0026 Further Reading\n\n- [`examples/client_credentials.rs`](examples/client_credentials.rs) — spins up an `httpmock`\n  server, builds a broker with the default reqwest client, and mirrors the Quickstart flow without\n  touching external networks.\n- [`examples/custom_transport.rs`](examples/custom_transport.rs) — shows how to register a custom\n  `TokenHttpClient` plus mapper so transports that do not use reqwest can participate in flows.\n- [`examples/start_authorization.rs`](examples/start_authorization.rs) — shows how to generate an\n  `AuthorizationSession`, persist/lookup `state`, and surface PKCE material around a redirect.\n- [`docs/DESIGN.md`](docs/DESIGN.md) — design outline plus the Release Overview section for the\n  MVP crate map, extension traits, observability model, and explicit out-of-scope decisions.\n- [`CHANGELOG.md`](CHANGELOG.md) — dated release notes (0.0.1 captures the MVP surface).\n- [`CONTRIBUTING.md`](CONTRIBUTING.md) — guardrails, quality gates, and reporting instructions.\n\n## Development Guardrails\n\nThe tests cover the reqwest-backed flows against an `httpmock` server plus the\nauthorization, refresh, and client-credentials flows end to end.\n\n## Support Me\n\nIf you find this project helpful and would like to support its development, you can buy me a coffee!\n\nYour support is greatly appreciated and motivates me to keep improving this project.\n\n- **Fiat**\n    - [Ko-fi](https://ko-fi.com/hack_ink)\n    - [爱发电](https://afdian.com/a/hack_ink)\n- **Crypto**\n    - **Bitcoin**\n        - `bc1pedlrf67ss52md29qqkzr2avma6ghyrt4jx9ecp9457qsl75x247sqcp43c`\n    - **Ethereum**\n        - `0x3e25247CfF03F99a7D83b28F207112234feE73a6`\n    - **Polkadot**\n        - `156HGo9setPcU2qhFMVWLkcmtCEGySLwNqa3DaEiYSWtte4Y`\n\nThank you for your support!\n\n## Appreciation\n\nWe would like to extend our heartfelt gratitude to the following projects and contributors:\n\nGrateful for the Rust community and the maintainers of `reqwest`, `oauth2`, `metrics`, and `tracing`, whose work makes this broker possible.\n\n## Additional Acknowledgements\n\n- TODO\n\n\u003cdiv align=\"right\"\u003e\n\n### License\n\n\u003csup\u003eLicensed under [GPL-3.0](LICENSE).\u003c/sup\u003e\n\n\u003c/div\u003e\n","funding_links":["https://ko-fi.com/hack_ink"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhack-ink%2Foauth2-broker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhack-ink%2Foauth2-broker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhack-ink%2Foauth2-broker/lists"}