totp debugging

This commit is contained in:
Graham McIntire 2026-01-29 09:14:48 -06:00
parent 9353a24760
commit 857c8d2196
3 changed files with 123 additions and 2 deletions

View file

@ -80,7 +80,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<Layouts.authenticated flash={@flash} current_scope={@current_scope}>
<div class="mx-auto max-w-2xl px-4 py-12">
<div class="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-8">
<div class="text-center mb-8">
@ -225,7 +225,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
</div>
</div>
</div>
</Layouts.app>
</Layouts.authenticated>
"""
end
end

61
search_totp_code.exs Normal file
View file

@ -0,0 +1,61 @@
alias Towerops.Accounts
# The code from the error
provided_code = "147273"
server_time = 1_769_699_437
IO.puts("=== Searching for when code #{provided_code} would be valid ===")
IO.puts("Server time: #{DateTime.from_unix!(server_time)}")
IO.puts("")
# Generate a fresh secret to test with
secret = Accounts.generate_totp_secret()
IO.puts("Testing with a fresh 20-byte secret...")
IO.puts("")
# Search ±1 hour (120 time steps of 30 seconds each)
found =
Enum.find_value(-120..120, fn offset ->
test_time = server_time + offset * 30
expected = NimbleTOTP.verification_code(secret, time: test_time)
if expected == provided_code do
%{
offset_seconds: offset * 30,
offset_minutes: Float.round(offset * 30 / 60, 1),
time: DateTime.from_unix!(test_time)
}
end
end)
case found do
nil ->
IO.puts("❌ Code #{provided_code} would NEVER be valid for a fresh 20-byte secret")
IO.puts("")
IO.puts("This means:")
IO.puts(" 1. Your authenticator app has a DIFFERENT secret than the server")
IO.puts(" 2. You need to:")
IO.puts(" - Delete the Towerops entry from your authenticator app")
IO.puts(" - Visit the enrollment page again (fresh)")
IO.puts(" - Scan the NEW QR code")
IO.puts(" - Enter the code immediately")
match ->
IO.puts("✓ Code #{provided_code} would be valid at:")
IO.puts(" Time: #{match.time}")
IO.puts(" Offset: #{match.offset_seconds} seconds (#{match.offset_minutes} minutes)")
IO.puts("")
cond do
abs(match.offset_seconds) <= 120 ->
IO.puts("⚠️ This is within the ±2 minute window - should have worked!")
abs(match.offset_seconds) <= 3600 ->
IO.puts("⏰ Clock drift: #{abs(match.offset_minutes)} minutes")
IO.puts(" Your device's clock is incorrect")
true ->
IO.puts("⏰ Extreme time difference: #{abs(match.offset_minutes)} minutes")
IO.puts(" Your device's clock is way off")
end
end

60
test_totp_flow.exs Normal file
View file

@ -0,0 +1,60 @@
alias Towerops.Accounts
alias Towerops.Accounts.User
# Simulate enrollment flow
secret = Accounts.generate_totp_secret()
user = %User{email: "test@example.com"}
IO.puts("=== TOTP Enrollment Flow Test ===")
IO.puts("")
IO.puts("1. Generated secret:")
IO.puts(" Bytes: #{byte_size(secret)}")
IO.puts(" Type: #{inspect(:io_lib.printable_list(secret))}")
# Generate URI (what goes in QR code)
uri = Accounts.generate_totp_uri(user, secret)
IO.puts("")
IO.puts("2. Generated URI:")
IO.puts(" #{uri}")
# Extract the Base32 secret from the URI
case Regex.run(~r/secret=([A-Z2-7]+)/, uri) do
[_, base32] ->
IO.puts("")
IO.puts("3. Extracted Base32 from URI:")
IO.puts(" Base32: #{base32}")
IO.puts(" Length: #{String.length(base32)} characters")
# Verify the Base32 can be decoded back
try do
decoded = Base.decode32!(base32, padding: false)
IO.puts("")
IO.puts("4. Decode verification:")
IO.puts(" Decoded bytes: #{byte_size(decoded)}")
IO.puts(" Matches original: #{decoded == secret}")
if decoded != secret do
IO.puts(" ERROR: Decoded secret doesn't match!")
IO.puts(" Original bytes: #{byte_size(secret)}")
IO.puts(" Decoded bytes: #{byte_size(decoded)}")
end
rescue
e ->
IO.puts("")
IO.puts("4. Decode ERROR: #{inspect(e)}")
end
_ ->
IO.puts("")
IO.puts("ERROR: Failed to extract Base32 from URI")
end
# Test verification
current_time = System.system_time(:second)
code = NimbleTOTP.verification_code(secret, time: current_time)
valid = Accounts.verify_totp(secret, code)
IO.puts("")
IO.puts("5. Verification test:")
IO.puts(" Generated code: #{code}")
IO.puts(" Validates: #{valid}")