# OAuth

PKCE primitives + init-URL builder. No secret, no signature — safe to use from any context that needs to construct an authorization URL. Pairs with `Client::exchange_oauth_code` on the trusted side.

```rust
use akira_billing::{
    generate_pkce_challenge, generate_oauth_state, build_oauth_init_url,
    BuildOauthInitUrl, PkceChallenge,
};
```

## PkceChallenge

```rust
pub struct PkceChallenge {
    pub verifier: String,
    pub challenge: String,
    pub method: &'static str,    // always "S256"
}
```

`verifier` is 48 bytes of CSPRNG → URL-safe base64 (no padding). `challenge` is `URL_SAFE_NO_PAD(sha256(verifier.as_bytes()))`. Persist `verifier` locally; transmit `challenge` to the billing app via the init URL.

## generate_pkce_challenge

```rust
pub fn generate_pkce_challenge() -> PkceChallenge;
```

Pure CPU work. No I/O. Safe to call on any task.

## generate_oauth_state

```rust
pub fn generate_oauth_state() -> String;
```

24 bytes of CSPRNG → URL-safe base64. Use it as the `state` query parameter; the provider echoes it back. Compare against the stored value before exchanging the code.

## build_oauth_init_url

```rust
pub fn build_oauth_init_url(opts: BuildOauthInitUrl<'_>) -> String;

pub struct BuildOauthInitUrl<'a> {
    pub base_url: &'a str,
    pub provider: &'a str,           // "google" | "github" | "apple" | "microsoft" | "gitlab" | "bitbucket"
    pub product: &'a str,
    pub redirect_uri: &'a str,
    pub code_challenge: &'a str,
    pub code_challenge_method: Option<&'a str>,    // default "S256"
    pub state: Option<&'a str>,
}
```

Returns `{base_url}/auth/{provider}?product=…&redirect_uri=…&code_challenge=…&code_challenge_method=…&state=…`.

- `base_url` trailing slash is stripped.
- Every value is URL-encoded via an in-module helper (RFC 3986 unreserved set).
- `state` is omitted when `None` — always pass one.
- Returns a `String` rather than `url::Url` so consumers without the `url` crate can use it freely. The `loopback` feature does pull `url` for the local server side.

## Full desktop flow (without loopback)

```rust
use akira_billing::{
    generate_pkce_challenge, generate_oauth_state, build_oauth_init_url, BuildOauthInitUrl,
    Client,
};

let pkce = generate_pkce_challenge();
let state = generate_oauth_state();

// Persist pkce.verifier + state for after the redirect.
store.put("pkce_verifier", &pkce.verifier)?;
store.put("oauth_state", &state)?;

let url = build_oauth_init_url(BuildOauthInitUrl {
    base_url: "https://billing.akira.foundation",
    provider: "github",
    product: "unified-dev",
    redirect_uri: "http://127.0.0.1:31337/cb",
    code_challenge: &pkce.challenge,
    code_challenge_method: None,
    state: Some(&state),
});

open_browser(&url).await?;
```

After the redirect returns to your callback handler:

```rust
let code = req.query_param("code")?;
let returned_state = req.query_param("state")?;

let expected_state = store.take("oauth_state")?;
let verifier = store.take("pkce_verifier")?;

anyhow::ensure!(returned_state == expected_state, "oauth_state_mismatch");

let mut client = Client::new(base_url, product, secret);
let exchange = client
    .exchange_oauth_code(OauthExchangePayload {
        code: &code,
        code_verifier: &verifier,
    })
    .await?;

// `client` now carries exchange.access_token.
```

## Loopback shortcut

The `loopback` feature provides `loopback_login` which binds a local server, opens the browser, waits for the callback, and exchanges the code in one call. Prefer that over hand-rolling the redirect for desktop apps. See [40-loopback](40-loopback.md).

## PKCE method override

`generate_pkce_challenge` always emits `S256`. Override `code_challenge_method` only when interoperating with a third-party client that cannot SHA-256 the verifier.

---

Navigation: [← Errors](12-errors.md) · **OAuth** · [License →](30-license.md)
