feat: add Accounts.verify_totp_only/2 function
This commit is contained in:
parent
92350173b0
commit
42565779bc
2 changed files with 88 additions and 0 deletions
|
|
@ -574,6 +574,51 @@ defmodule Towerops.Accounts do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Verifies a TOTP code for a user, rejecting recovery codes.
|
||||
|
||||
This is used for sudo mode verification where we want to ensure
|
||||
the user has access to their authenticator device, not just a recovery code.
|
||||
|
||||
Returns `{:ok, user}` on success with valid TOTP code.
|
||||
Returns `{:error, :recovery_code_not_allowed}` if a recovery code is provided.
|
||||
Returns `{:error, reason}` for invalid TOTP codes.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> verify_totp_only(user, "123456")
|
||||
{:ok, %User{}}
|
||||
|
||||
iex> verify_totp_only(user, "ABCD-EFGH-IJKL")
|
||||
{:error, :recovery_code_not_allowed}
|
||||
|
||||
iex> verify_totp_only(user, "000000")
|
||||
{:error, :invalid_code}
|
||||
"""
|
||||
def verify_totp_only(%User{} = user, code) when is_binary(code) do
|
||||
# TOTP codes are exactly 6 numeric digits
|
||||
# Recovery codes contain letters and/or are longer than 6 digits
|
||||
numeric_only = String.replace(code, ~r/[^0-9]/, "")
|
||||
|
||||
cond do
|
||||
# Code contains non-numeric characters (recovery codes have letters)
|
||||
String.length(numeric_only) != String.length(code) ->
|
||||
{:error, :recovery_code_not_allowed}
|
||||
|
||||
# Code is longer than 6 digits
|
||||
String.length(numeric_only) > 6 ->
|
||||
{:error, :recovery_code_not_allowed}
|
||||
|
||||
# Looks like a TOTP code, verify it
|
||||
true ->
|
||||
case verify_user_mfa(user, code) do
|
||||
{:ok, user, :totp} -> {:ok, user}
|
||||
{:ok, _user, :recovery_code} -> {:error, :recovery_code_not_allowed}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Settings
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
43
test/towerops/accounts/verify_totp_only_test.exs
Normal file
43
test/towerops/accounts/verify_totp_only_test.exs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
defmodule Towerops.Accounts.VerifyTotpOnlyTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
import Towerops.AccountsFixtures
|
||||
|
||||
alias Towerops.Accounts
|
||||
|
||||
describe "verify_totp_only/2" do
|
||||
setup do
|
||||
user = user_fixture()
|
||||
|
||||
# Create TOTP device
|
||||
{:ok, device, _secret} =
|
||||
Accounts.create_totp_device(user.id, "Test Device")
|
||||
|
||||
%{user: user, device: device}
|
||||
end
|
||||
|
||||
test "accepts valid TOTP codes", %{user: user, device: device} do
|
||||
code = NimbleTOTP.verification_code(device.totp_secret)
|
||||
|
||||
assert {:ok, returned_user} = Accounts.verify_totp_only(user, code)
|
||||
assert returned_user.id == user.id
|
||||
end
|
||||
|
||||
test "rejects invalid TOTP codes", %{user: user} do
|
||||
assert {:error, :invalid_code} = Accounts.verify_totp_only(user, "000000")
|
||||
end
|
||||
|
||||
test "rejects recovery codes", %{user: user} do
|
||||
# Recovery codes are longer than 6 digits
|
||||
recovery_code = "ABCD-EFGH-IJKL"
|
||||
|
||||
assert {:error, :recovery_code_not_allowed} =
|
||||
Accounts.verify_totp_only(user, recovery_code)
|
||||
end
|
||||
|
||||
test "rejects codes longer than 6 digits", %{user: user} do
|
||||
assert {:error, :recovery_code_not_allowed} =
|
||||
Accounts.verify_totp_only(user, "1234567890")
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue