Added module for useful authentication actions
This commit is contained in:
190
lib/sukaato/auth.ex
Normal file
190
lib/sukaato/auth.ex
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
defmodule Sukaato.Auth do
|
||||||
|
import Argon2
|
||||||
|
import Ecto.Query
|
||||||
|
require Integer
|
||||||
|
alias Sukaato.{Repo, User}
|
||||||
|
# alias Sukaato.StandardQueries
|
||||||
|
use SukaatoWeb, :controller
|
||||||
|
|
||||||
|
@rel_proj_root "../.."
|
||||||
|
@site_config_file Path.expand(@rel_proj_root <> "/site.toml", __DIR__)
|
||||||
|
@site_config Toml.decode_file(@site_config_file)
|
||||||
|
@admin_username elem(@site_config, 1)["site"]["username"]
|
||||||
|
# @fqdn elem(@site_config, 1)["site"]["fqdn"]
|
||||||
|
# @admin_files_path Path.expand(@rel_proj_root <> "/priv/static/files/users/" <> @admin_username, __DIR__)
|
||||||
|
# @markdown_posts_path Path.expand(@rel_proj_root <> "/lib/sukaato_web/controllers/page_md/users/" <> @admin_username <> "/posts", __DIR__)
|
||||||
|
|
||||||
|
@tables %{
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate_user_session(conn, user_id \\ @admin_username, authenticated, session_key \\ "login_token") when is_binary(user_id) and is_boolean(authenticated) and is_binary(session_key) do
|
||||||
|
if authenticated do
|
||||||
|
user_token = :crypto.strong_rand_bytes(32) |> Base.encode64 |> binary_part(0, 32)
|
||||||
|
|
||||||
|
conn = put_session(conn, session_key, user_id <> ":" <> user_token)
|
||||||
|
conn
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_fido_challenge(conn, user_id \\ @admin_username) when is_binary(user_id) do
|
||||||
|
query = from u in @tables.user, select: {u.fido_enabled, u.fido_active, u.fido_priority, u.fido_creds, u.fido_keys}, where: u.username == ^user_id or u.email == ^user_id
|
||||||
|
result = Repo.one(query)
|
||||||
|
fido_required = elem(result, 0) and elem(result, 1)
|
||||||
|
fido_pairs = if fido_required, do: Enum.zip([elem(result, 3), elem(result, 4)]), else: []
|
||||||
|
fido_pairs = if length(fido_pairs) > 0 and elem(result, 2) != nil, do: [Enum.at(fido_pairs, elem(result, 2))], else: fido_pairs
|
||||||
|
challenge = if fido_required, do: Wax.new_authentication_challenge(allow_credentials: fido_pairs), else: nil
|
||||||
|
# @TODO add preprocessing of challenge data to match appropriate form
|
||||||
|
# @NOTE see requirements for input params of JS Web Authentication as guide: https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse#instance_properties
|
||||||
|
|
||||||
|
if challenge != nil do
|
||||||
|
conn = put_session(conn, "fido_challenge", challenge)
|
||||||
|
{conn, challenge, fido_pairs}
|
||||||
|
else
|
||||||
|
{conn, challenge, fido_pairs}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_credentials(conn, fields \\ [], source \\ :body_params) when is_list(fields) and is_atom(source) do
|
||||||
|
avail_conn_params = if length(fields) > 0, do: Enum.filter(fields, fn f -> Map.has_key?(Map.get(conn, source), f) end), else: []
|
||||||
|
# IO.inspect(conn.body_params)
|
||||||
|
|
||||||
|
if length(avail_conn_params) > 0 do
|
||||||
|
response = Enum.map(avail_conn_params, fn p -> {p, Map.get(conn, source)[p]} end)
|
||||||
|
response = Map.new(response)
|
||||||
|
response
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp authflow_assessment(result, mode) when is_atom(mode) do
|
||||||
|
cond do
|
||||||
|
is_tuple(result) ->
|
||||||
|
result = Tuple.to_list(result)
|
||||||
|
|
||||||
|
evened_result = Enum.chunk_every(result, 2)
|
||||||
|
results = Enum.map(evened_result, fn r -> apply(Enum, mode, [r]) end)
|
||||||
|
whether_required = apply(Enum, mode, [results])
|
||||||
|
|
||||||
|
# evened_result = if Integer.is_even(length(result)), do: result, else: Enum.drop(result, -1)
|
||||||
|
# ceiling = length(evened_result) - 1
|
||||||
|
#
|
||||||
|
# indices = 0..ceiling//2
|
||||||
|
# # indices = Enum.to_list(indices)
|
||||||
|
# enableds = Enum.map(indices, fn i -> Enum.fetch!(evened_result, i - 1) end)
|
||||||
|
# IO.inspect(enableds)
|
||||||
|
# actives = Enum.map(indices, fn i -> Enum.fetch!(evened_result, i) end)
|
||||||
|
# IO.inspect(actives)
|
||||||
|
# total_range = if length(enableds) >= length(actives), do: 0..length(enableds), else: length(enableds) < length(actives)
|
||||||
|
# total_range = if is_boolean(total_range) and total_range, do: 0..length(actives), else: 0..0
|
||||||
|
# conjuncts = Enum.zip([total_range, enableds, actives])
|
||||||
|
# conjunctions = Enum.map(conjuncts, fn c -> {elem(c, 0), elem(c, 1) and elem(c, 2)} end)
|
||||||
|
|
||||||
|
# last_item = if length(result) > length(evened_result), do: Enum.at(result, -1), else: nil
|
||||||
|
# last_item = if is_nil(last_item), do: nil, else: (if is_boolean(last_item), do: last_item, else: !is_nil(last_item))
|
||||||
|
# last_conjunction = if is_nil(last_item), do: [], else: [{length(conjunctions), last_item}]
|
||||||
|
# conjunctions = Enum.concat(conjunctions, last_conjunction)
|
||||||
|
|
||||||
|
# requisites = Enum.filter(conjunctions, fn c -> elem(c, 1) end)
|
||||||
|
# conjunctions = Enum.map(conjunctions, fn c -> elem(c, 1) end)
|
||||||
|
# # whether_required = Enum.all?(conjunctions)
|
||||||
|
# whether_required = apply(Enum, mode, [conjunctions])
|
||||||
|
|
||||||
|
# {:ok, whether_required, requisites}
|
||||||
|
{:ok, whether_required, result}
|
||||||
|
!is_tuple(result) ->
|
||||||
|
whether_required = if is_boolean(result), do: result, else: !is_nil(result)
|
||||||
|
|
||||||
|
{:ok, whether_required, [result]}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_authflow_required?(user_id \\ @admin_username, mode \\ :all?, result \\ nil) when is_binary(user_id) and is_atom(mode) do
|
||||||
|
query = if is_nil(result), do: (from u in @tables.user, select: {u.totp_active, u.totp_enabled, u.fido_active, u.fido_enabled}, where: u.username == ^user_id or u.email == ^user_id), else: nil
|
||||||
|
result = if is_nil(query), do: result, else: Repo.one(query)
|
||||||
|
result = if is_struct(result) and match?(%Ecto.Query{}, result), do: Repo.one(result), else: result
|
||||||
|
result = authflow_assessment(result, mode)
|
||||||
|
required = elem(result, 1)
|
||||||
|
required
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
defp pass_auth(password, stored_password) when is_binary(password) and is_binary(stored_password) do
|
||||||
|
authenticated = verify_pass(password, stored_password)
|
||||||
|
authenticated
|
||||||
|
end
|
||||||
|
|
||||||
|
defp totp_auth(totp_code, totp_secret) when is_binary(totp_code) do
|
||||||
|
true_secret = Base.decode32!(totp_secret)
|
||||||
|
authenticated = NimbleTOTP.valid?(true_secret, totp_code)
|
||||||
|
authenticated
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fido_auth(challenge, credential_id, auth_data_bin, sig, client_data_json_raw) do
|
||||||
|
# @TODO add preprocessing of input params to match appropriate nested data type
|
||||||
|
# @NOTE see requirements for parameters of below as guide: https://hexdocs.pm/wax_/Wax.html#authenticate/6
|
||||||
|
result = Wax.authenticate(credential_id, auth_data_bin, sig, client_data_json_raw, challenge)
|
||||||
|
|
||||||
|
if elem(result, 0) == :ok do
|
||||||
|
authenticated = true
|
||||||
|
authenticated
|
||||||
|
else
|
||||||
|
authenticated = false
|
||||||
|
authenticated
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def password_bouncer(conn, user_id \\ @admin_username, password, result \\ nil) when is_binary(user_id) and is_binary(password) do
|
||||||
|
query = from u in @tables.user, select: u.password, where: u.username == ^user_id or u.email == ^user_id
|
||||||
|
result = if is_nil(result), do: Repo.one(query), else: result
|
||||||
|
stored_password = result
|
||||||
|
authenticated = if is_nil(stored_password), do: false, else: pass_auth(password, stored_password)
|
||||||
|
|
||||||
|
if authenticated do
|
||||||
|
conn = generate_user_session(conn, user_id, authenticated, "pass_token")
|
||||||
|
{conn, authenticated, result}
|
||||||
|
else
|
||||||
|
{conn, authenticated, result}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def totp_bouncer(conn, user_id \\ @admin_username, code, result \\ nil) when is_binary(user_id) and is_binary(user_id) do
|
||||||
|
query = from u in @tables.user, select: {u.totp_active, u.totp_enabled, u.totp_secret, u.ltotp}, where: u.username == ^user_id or u.email == ^user_id
|
||||||
|
result = if is_nil(result), do: Repo.one(query), else: result
|
||||||
|
requirement_query = from u in @tables.user, select: {u.totp_active, u.totp_enabled}, where: u.username == ^user_id or u.email == ^user_id
|
||||||
|
totp_required = is_authflow_required?(user_id, :all?, (requirement_query))
|
||||||
|
secret = if totp_required, do: elem(result, 2), else: nil
|
||||||
|
# last_totp_chall = if totp_required, do: elem(result, 3), else: nil
|
||||||
|
authenticated = if totp_required, do: totp_auth(code, secret), else: true
|
||||||
|
|
||||||
|
if authenticated do
|
||||||
|
conn = generate_user_session(conn, user_id, authenticated, "2fa_token")
|
||||||
|
{conn, authenticated, result}
|
||||||
|
else
|
||||||
|
{conn, authenticated, result}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fido_bouncer(conn, user_id \\ @admin_username, webauthn_response \\ %{}, result \\ nil) when is_binary(user_id) and (is_map(webauthn_response) or is_struct(webauthn_response)) do
|
||||||
|
query = from u in @tables.user, select: {u.fido_active, u.fido_enabled, u.fido_priority, u.fido_creds, u.fido_keys}, where: u.username == ^user_id or u.email == ^user_id
|
||||||
|
result = if result == nil, do: Repo.one(query), else: result
|
||||||
|
fido_required = if result != nil, do: is_authflow_required?(user_id, result), else: false
|
||||||
|
# conn = fetch_session(conn)
|
||||||
|
challenge = if fido_required, do: get_session(conn, "fido_challenge"), else: nil
|
||||||
|
cred_id = if fido_required, do: webauthn_response.credential_id, else: nil
|
||||||
|
auth_data = if fido_required, do: webauthn_response.authenticator_data, else: nil
|
||||||
|
sig = if fido_required, do: webauthn_response.signature, else: nil
|
||||||
|
raw_client_json = if fido_required, do: webauthn_response.raw_client_json, else: nil
|
||||||
|
authenticated = if fido_required, do: fido_auth(challenge, cred_id, auth_data, sig, raw_client_json), else: true
|
||||||
|
|
||||||
|
if authenticated do
|
||||||
|
conn = generate_user_session(conn, user_id, authenticated, "2fa_token")
|
||||||
|
{conn, authenticated, result}
|
||||||
|
else
|
||||||
|
{conn, authenticated, result}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Reference in New Issue
Block a user