@@ -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