totp debugging
This commit is contained in:
parent
9353a24760
commit
857c8d2196
3 changed files with 123 additions and 2 deletions
|
|
@ -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
61
search_totp_code.exs
Normal 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
60
test_totp_flow.exs
Normal 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}")
|
||||
Loading…
Add table
Reference in a new issue