Current section

Files

Jump to
indieweb lib indieweb auth default code.ex
Raw

lib/indieweb/auth/default/code.ex

defmodule IndieWeb.Auth.DefaultCodeAdapter do
@moduledoc """
Provides a implementation of the stateful parts of IndieAuth.
"""
@behaviour IndieWeb.Auth.Code.Adapter
@impl true
def generate() do
raw_code = :crypto.strong_rand_bytes(16)
:crypto.hash(:sha256, raw_code) |> Base.url_encode64(padding: false)
end
@impl true
def generate_challenge(options) do
# This should follow the logic at https://tools.ietf.org/html/rfc7636#section-4.1
verifier = :crypto.strong_rand_bytes(16) |> Base.encode32(padding: false)
method = Keyword.get(options, :method, "s256")
challenge = verification_method(method).(verifier)
{verifier, challenge, method}
end
@impl true
def persist(code, client_id, redirect_uri, args, options) do
code_age = Keyword.get(options, :code_age, 10 * 60)
IndieWeb.Cache.set(
"code:#{code}",
[client_id: client_id, redirect_uri: redirect_uri, params: args],
[expire: code_age],
options
)
end
@impl true
def persist_challenge(code, challenge, method, options) do
code_challenge_key = "pkce:for-code:#{code}"
code_age = Keyword.get(options, :code_age, 10 * 60)
IndieWeb.Cache.set(
code_challenge_key,
[challenge: challenge, challenge_method: method],
[expire: code_age],
options
)
end
@impl true
def verify(code, client_id, redirect_uri, options) do
case IndieWeb.Cache.get("code:#{code}", nil, options) do
nil ->
{:error, :code_not_found}
[client_id: ^client_id, redirect_uri: ^redirect_uri, params: params] ->
{:ok, params}
params ->
error =
cond do
params[:redirect_uri] != redirect_uri -> :mismatched_redirect_uri
params[:client_id] != client_id -> :mismatched_client_id
true -> :unexpected_error
end
{:error, error}
end
end
@impl true
def verify_challenge(code_verifier, code, options) do
code_challenge_key = "pkce:for-code:#{code}"
case IndieWeb.Cache.get(code_challenge_key, nil, options) do
nil ->
{:error, :code_challenge_not_found}
[challenge: challenge, challenge_method: method] ->
generated_challenge = verification_method(method).(code_verifier)
if generated_challenge == challenge do
:ok
else
{:error, :code_verifier_mismatch}
end
invalid_challenge ->
{:error, :invalid_code_challenge_method, arg: invalid_challenge}
end
end
defp verification_method(method) do
case String.downcase(method) do
"s256" ->
fn verifier -> Base.url_encode64(:crypto.hash(:sha256, verifier), padding: false) end
"s512" ->
fn verifier -> Base.url_encode64(:crypto.hash(:sha512, verifier), padding: false) end
"md5" ->
fn verifier -> Base.url_encode64(:crypto.hash(:md5, verifier), padding: false) end
_ ->
fn _verifier -> nil end
end
end
@impl true
def destroy(code, options \\ []) do
IndieWeb.Cache.delete("code:#{code}", options)
end
end