defmodule Immich.API.PKCE do
@moduledoc """
Generates PKCE parameters for the Immich OAuth flow.
"""
@enforce_keys [:code_verifier, :code_challenge, :state]
defstruct @enforce_keys
@type t :: %__MODULE__{
code_verifier: String.t(),
code_challenge: String.t(),
state: String.t()
}
@alphabet ~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
@alphabet_tuple List.to_tuple(@alphabet)
@alphabet_length tuple_size(@alphabet_tuple)
@default_verifier_length 64
@default_state_length 32
@doc """
Generates a fresh PKCE triple.
Options:
* `:verifier_length` - target length for the code verifier (default: #{@default_verifier_length})
* `:state_length` - target length for the state token (default: #{@default_state_length})
* `:rand_fun` - function that receives a positive integer and returns that many random bytes
"""
@spec new(keyword()) :: t()
def new(opts \\ []) do
rand_fun = Keyword.get(opts, :rand_fun, &:crypto.strong_rand_bytes/1)
code_verifier =
opts
|> Keyword.get(:verifier_length, @default_verifier_length)
|> random_string(rand_fun)
state =
opts
|> Keyword.get(:state_length, @default_state_length)
|> random_string(rand_fun)
%__MODULE__{
code_verifier: code_verifier,
code_challenge: challenge(code_verifier),
state: state
}
end
@doc """
Generates the PKCE code challenge by applying the `S256` transformation
(`base64url(SHA256(verifier))`) recommended by RFC 7636 §4.2 for public
clients. The same guidance is echoed by the OWASP OAuth 2.0 cheat sheet, so
this helper always uses SHA-256 plus URL-safe base64 encoding.
"""
@spec challenge(String.t()) :: String.t()
def challenge(code_verifier) when is_binary(code_verifier) do
:crypto.hash(:sha256, code_verifier)
|> Base.url_encode64(padding: false)
end
defp random_string(length, _rand_fun) when length <= 0 do
raise ArgumentError, "length must be positive"
end
defp random_string(length, rand_fun) do
rand_fun.(length)
|> :binary.bin_to_list()
|> Enum.map(fn byte -> alphabet_char(rem(byte, @alphabet_length)) end)
|> to_string()
end
defp alphabet_char(index), do: elem(@alphabet_tuple, index)
end