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

RFC 7636 - Proof Key for Code Exchange (PKCE).

PKCE binds an authorization code to a secret the client generates per
request, so a stolen code is useless without the matching secret. At the
authorization request the client sends a `code_challenge` (a transform
of a freshly generated `code_verifier`); at the token request it sends
the `code_verifier`, and the server recomputes the challenge and
compares.

## S256 only

This module implements the `S256` method exclusively:

    code_challenge = base64url(SHA-256(code_verifier)), no padding

The `plain` method (RFC 7636 §4.2, where the challenge *is* the
verifier) is deliberately not supported: it offers no protection
against an attacker who can read the authorization request, and modern
guidance (OAuth 2.0 Security BCP) requires S256. `verify/3` rejects any
method other than `"S256"` with `{:error, :unsupported_method}`, so a
downgrade to `plain` cannot succeed.

## Verifier and challenge shapes

  * A `code_verifier` is 43 to 128 characters from the unreserved set
    `[A-Za-z0-9-._~]` (RFC 7636 §4.1).
  * An `S256` `code_challenge` is the canonical 43-character
    base64url-no-pad encoding of a 32-byte SHA-256 digest - the same
    shape `Attesto.Thumbprint` validates.

The comparison at the token endpoint is constant-time
(`Attesto.SecureCompare`).

# `challenge`

```elixir
@spec challenge(String.t()) :: {:ok, String.t()} | {:error, :invalid_verifier}
```

Compute the `S256` `code_challenge` for a `code_verifier`.

Returns `{:ok, challenge}` for a well-formed verifier (43-128 unreserved
characters) or `{:error, :invalid_verifier}` otherwise. The challenge is
`base64url(SHA-256(verifier))` without padding.

# `method`

```elixir
@spec method() :: String.t()
```

The only supported code-challenge method, `"S256"`.

# `valid_challenge?`

```elixir
@spec valid_challenge?(term()) :: boolean()
```

Returns `true` iff `value` is a well-formed `S256` `code_challenge`:
the canonical 43-character base64url-no-pad encoding of a 32-byte
SHA-256 digest. Delegates to `Attesto.Thumbprint.valid?/1`.

# `valid_verifier?`

```elixir
@spec valid_verifier?(term()) :: boolean()
```

Returns `true` iff `value` is a well-formed `code_verifier`: 43 to 128
characters drawn from the RFC 7636 §4.1 unreserved set
`[A-Za-z0-9-._~]`.

# `verify`

```elixir
@spec verify(String.t(), String.t(), String.t()) ::
  :ok
  | {:error,
     :unsupported_method | :invalid_verifier | :invalid_challenge | :mismatch}
```

Verify a presented `code_verifier` against the stored `code_challenge`.

`method` defaults to `"S256"` and MUST be `"S256"`; any other value
(including `"plain"`) returns `{:error, :unsupported_method}`.

Returns:

  * `:ok` if the verifier is well-formed and its `S256` challenge
    matches `code_challenge` (constant-time compare).
  * `{:error, :unsupported_method}` if `method` is not `"S256"`.
  * `{:error, :invalid_verifier}` if the verifier is not 43-128
    unreserved characters.
  * `{:error, :invalid_challenge}` if the stored challenge is not a
    canonical 43-character base64url SHA-256 value (it could never have
    been produced by `challenge/1`, so a match is impossible and the
    stored value is corrupt).
  * `{:error, :mismatch}` if a well-formed verifier does not match a
    well-formed challenge.

---

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