Attesto.PKCE (Attesto v0.5.0)

Copy Markdown View Source

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).

Summary

Functions

Compute the S256 code_challenge for a code_verifier.

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

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.

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 a presented code_verifier against the stored code_challenge.

Functions

challenge(code_verifier)

@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()

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

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

valid_challenge?(value)

@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?(value)

@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(code_challenge, code_verifier, method \\ "S256")

@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.