defmodule OhMyExOhIrcd.IRC.Auth do
@moduledoc """
Bridges IRC authentication into the Phoenix `phx.gen.auth` Accounts
context.
Two paths are supported:
* **SASL PLAIN** (IRCv3) — the modern flow. Client sends
`AUTHENTICATE PLAIN` then a base64 blob `\\0<email>\\0<password>`.
* **PASS** (classic IRC) — sent before NICK/USER. We accept the
forms `email:password` and `email password`. This is a fallback
for clients that don't speak SASL.
Both look the account up by email and check its bcrypt hash.
"""
require Logger
alias OhMyExOhIrcd.Accounts
alias OhMyExOhIrcd.Accounts.User
@doc """
Decode a SASL PLAIN payload and verify it against Accounts.
Returns `{:ok, %User{}}`, `{:error, :malformed}`, or
`{:error, {reason, email}}` with `reason` one of `:no_such_account`,
`:no_password` (the account only signs in by magic link) or
`:wrong_password`.
"""
def verify_sasl_plain(payload) when is_binary(payload) do
with {:ok, decoded} <- Base.decode64(payload),
[_authzid, authcid, password] <- :binary.split(decoded, <<0>>, [:global]) do
verify(authcid, password)
else
_ -> {:error, :malformed}
end
end
@doc """
Parse a PASS argument and verify it. Supports `email:password` and
`email password` forms.
"""
def verify_pass(arg) when is_binary(arg) do
case parse_pass(arg) do
{:ok, email, password} -> verify(email, password)
:error -> {:error, :malformed}
end
end
defp parse_pass(arg) do
cond do
String.contains?(arg, ":") ->
[email, password] = String.split(arg, ":", parts: 2)
{:ok, email, password}
String.contains?(arg, " ") ->
[email, password] = String.split(arg, " ", parts: 2)
{:ok, email, password}
true ->
:error
end
end
# Which of the three failures it was, for the log: the client only ever
# hears "authentication failed". The misses still run a dummy hash, so
# response time doesn't tell an account that exists from one that doesn't.
defp verify(email, password) do
case Accounts.get_user_by_email(email) do
%User{hashed_password: hash} = user when is_binary(hash) ->
if User.valid_password?(user, password),
do: {:ok, user},
else: {:error, {:wrong_password, email}}
%User{} ->
User.valid_password?(nil, password)
{:error, {:no_password, email}}
nil ->
User.valid_password?(nil, password)
{:error, {:no_such_account, email}}
end
end
@doc """
Log a failed SASL or PASS attempt: the mechanism, the account with its
local part masked, the client's host and why. Never the password.
"""
def log_failure(mechanism, error, host) do
{reason, who} =
case error do
{reason, email} when is_binary(email) -> {reason, mask(email)}
reason -> {reason, "-"}
end
Logger.warning("#{mechanism} authentication failed for #{who} from #{host || "?"}: #{reason}")
end
# "gmp@gmp.io" -> "g**@gmp.io": enough to tell which account was tried
# without writing addresses into the log.
defp mask(email) do
case String.split(email, "@", parts: 2) do
[<<first::utf8, rest::binary>>, domain] ->
<<first::utf8>> <> String.duplicate("*", String.length(rest)) <> "@" <> domain
_ ->
"(not an email)"
end
end
end
neiam
/oh-my-ex-oh-ircd
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
3.3 KiB · text
History
4b6fe3b