Skip to content

Commit 306e4cd

Browse files
committed
Run more checks to ensure otpauth URI is valid
1 parent 5ec3d7d commit 306e4cd

1 file changed

Lines changed: 70 additions & 0 deletions

File tree

lib/nimble_totp.ex

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,79 @@ defmodule NimbleTOTP do
140140
@typedoc "Options for `valid?/3`."
141141
@type validate_option() :: {:since, time() | nil}
142142

143+
@doc """
144+
Generate the URI to be encoded in the QR code. Contrary to NimbleTOTP.otpauth_uri/2,
145+
this function can return an error if the label or the issuer are not well-formed.
146+
Also, it ensures that the issuer, when present, is placed twice in the resulting URI,
147+
as recommended by the
148+
[reference documentation](https://github.com/google/google-authenticator/wiki/Key-Uri-Format).
149+
150+
## Examples
151+
152+
iex> NimbleTOTP.otpauth("alice", "abcd", issuer: "Acme")
153+
{:ok, "otpauth://totp/Acme:alice?secret=MFRGGZA&issuer=Acme"}
154+
155+
iex> NimbleTOTP.otpauth("alice", "abcd")
156+
{:ok, "otpauth://totp/alice?secret=MFRGGZA"}
157+
158+
iex> NimbleTOTP.otpauth("Acme:alice", "abcd")
159+
{:ok, "otpauth://totp/Acme:alice?secret=MFRGGZA&issuer=Acme"}
160+
161+
iex> NimbleTOTP.otpauth("Wonderland:alice", "abcd", issuer: "Acme")
162+
{:error, :issuer_mismatch}
163+
164+
iex> NimbleTOTP.otpauth("Acme:Alice:Wonderland", "abcd")
165+
{:error, :too_many_colons}
166+
"""
167+
@spec otpauth(String.t(), String.t(), keyword()) :: {:ok, String.t()} | {:error, atom}
168+
def otpauth(label, secret, uri_params \\ []) when is_binary(label) and is_binary(secret) do
169+
with {:ok, label, uri_params} <- label_and_issuer(label, uri_params),
170+
do: {:ok, otpauth_uri(label, secret, uri_params)}
171+
end
172+
173+
@spec label_and_issuer(String.t(), keyword()) :: {:ok, String.t(), keyword()} | {:error, atom}
174+
defp label_and_issuer(label, uri_params) do
175+
from_label =
176+
case String.split(label, ":") do
177+
[label] -> {:ok, label, nil}
178+
[issuer, label] -> {:ok, label, issuer}
179+
_ -> {:error, :too_many_colons}
180+
end
181+
182+
from_uri_params =
183+
with {:ok, issuer} <- Keyword.fetch(uri_params, :issuer),
184+
[issuer] <- String.split(issuer, ":") do
185+
{:ok, issuer}
186+
else
187+
:error -> {:ok, nil}
188+
_ -> {:error, :too_many_colons}
189+
end
190+
191+
with {:ok, label, issuer_label} <- from_label,
192+
{:ok, issuer} <- from_uri_params do
193+
case {label, issuer_label || issuer, issuer} do
194+
{label, nil, nil} ->
195+
{:ok, label, uri_params}
196+
197+
{label, issuer, nil} ->
198+
{:ok, "#{issuer}:#{label}", Keyword.put(uri_params, :issuer, issuer)}
199+
200+
{label, issuer, issuer} ->
201+
{:ok, "#{issuer}:#{label}", uri_params}
202+
203+
_ ->
204+
{:error, :issuer_mismatch}
205+
end
206+
end
207+
end
208+
143209
@doc """
144210
Generate the URI to be encoded in the QR code.
145211
212+
This function will not fail even if the label is not well-formed (e.g., contains
213+
multiple colons), or if the issuer present in the label and the one given in the
214+
keyword do not match. Use NimbleTOTP.otpauth/2 if you need stricter control.
215+
146216
## Examples
147217
148218
iex> NimbleTOTP.otpauth_uri("Acme:alice", "abcd", issuer: "Acme")

0 commit comments

Comments
 (0)