Login, password hashing, and the two-factor handshake.
Picnic's login endpoint expects the password as an MD5 hex digest —
that is Picnic's scheme, not this library's choice; MD5 offers no real
protection and the value should be treated exactly like the plaintext
password. hash_password/1 also accepts an already-hashed value, and a
previously obtained token can skip login entirely via
Picnic.Client.new(auth_token: token).
The auth token is returned by the API in the x-picnic-auth response
header and stored on the client struct; subsequent requests send it
automatically.
Two-factor authentication
Accounts with 2FA enabled don't log in invisibly. login/4 returns
{:error, %Picnic.Error{category: :auth, reason: :two_factor_required}} —
note that it hands back an error, not a client. The error carries the
provisional client needed to finish the handshake, so feed the login result
straight into the 2FA calls:
result = Picnic.login(client, email, password)
# {:error, %Picnic.Error{reason: :two_factor_required}}
:ok = Picnic.Auth.generate_2fa(result)
{:ok, client} = Picnic.Auth.verify_2fa(result, code_from_sms)Pass send_2fa: true to have login/4 request the code itself, which
collapses the flow to two steps. It is opt-in because sending a code is a
side effect — the default keeps one login to one request, and leaves the
delivery channel your choice:
result = Picnic.login(client, email, password, send_2fa: true)
{:ok, client} = Picnic.Auth.verify_2fa(result, code_from_sms)The provisional token is also on the error as error.raw.auth_token, for
callers who need to carry it across processes rather than pipe the result.
Summary
Types
Anything generate_2fa/2 and verify_2fa/2 accept: a client, or the result
of login/4 passed through unopened.
Functions
Requests a second-factor code to be sent. Options: :channel (default "SMS").
MD5-hashes a plaintext password into the lowercase hex digest Picnic's login endpoint expects. A value that already looks like an MD5 digest (32 hex characters) is passed through unchanged, so pre-hashed secrets can be stored instead of plaintext.
Logs in and returns a client carrying the auth token.
Verifies a second-factor code and returns the fully authenticated client.
Types
@type resumable() :: Picnic.Client.t() | {:ok, Picnic.Client.t()} | {:error, Picnic.Error.t()}
Anything generate_2fa/2 and verify_2fa/2 accept: a client, or the result
of login/4 passed through unopened.
Functions
@spec generate_2fa( resumable(), keyword() ) :: :ok | {:error, Picnic.Error.t()}
Requests a second-factor code to be sent. Options: :channel (default "SMS").
Takes either a client carrying the provisional token or the login/4 result
that carries it — see the module documentation. Any other error is returned
unchanged, so a failed login propagates rather than being masked.
MD5-hashes a plaintext password into the lowercase hex digest Picnic's login endpoint expects. A value that already looks like an MD5 digest (32 hex characters) is passed through unchanged, so pre-hashed secrets can be stored instead of plaintext.
@spec login(Picnic.Client.t(), String.t(), String.t(), keyword()) :: {:ok, Picnic.Client.t()} | {:error, Picnic.Error.t()}
Logs in and returns a client carrying the auth token.
Returns {:error, %Picnic.Error{category: :auth, reason: :two_factor_required}}
when the account needs a second factor; pipe that result into
generate_2fa/2 and verify_2fa/2 to finish the handshake.
Options
:send_2fa— request the second-factor code as part of logging in, instead of leavinggenerate_2fa/2to the caller. Defaults tofalse.:channel— delivery channel for that code, default"SMS". Only used withsend_2fa: true.
@spec verify_2fa(resumable(), String.t()) :: {:ok, Picnic.Client.t()} | {:error, Picnic.Error.t()}
Verifies a second-factor code and returns the fully authenticated client.
Like generate_2fa/2, takes a client or the login/4 result. A login that
needed no second factor passes through as {:ok, client}, so the call is
safe to leave in a pipeline; any other error is returned unchanged.