Skip to content

Commit

Permalink
Ignore non-standard otpauth parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
kislyuk committed May 4, 2024
1 parent b87baea commit 249674b
Show file tree
Hide file tree
Showing 5 changed files with 23 additions and 24 deletions.
6 changes: 2 additions & 4 deletions src/pyotp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,11 @@ def parse_uri(uri: str) -> OTP:
otp_data["interval"] = int(value)
elif key == "counter":
otp_data["initial_count"] = int(value)
elif key != "image":
raise ValueError("{} is not a valid parameter".format(key))


if encoder != "steam":
if digits is not None and digits not in [6, 7, 8]:
raise ValueError("Digits may only be 6, 7, or 8")

if not secret:
raise ValueError("No secret found in URI")

Expand Down
4 changes: 2 additions & 2 deletions src/pyotp/hotp.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def provisioning_uri(
name: Optional[str] = None,
initial_count: Optional[int] = None,
issuer_name: Optional[str] = None,
image: Optional[str] = None,
**kwargs,
) -> str:
"""
Returns the provisioning URI for the OTP. This can then be
Expand All @@ -79,5 +79,5 @@ def provisioning_uri(
issuer=issuer_name if issuer_name else self.issuer,
algorithm=self.digest().name,
digits=self.digits,
image=image,
**kwargs,
)
7 changes: 2 additions & 5 deletions src/pyotp/totp.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,7 @@ def verify(self, otp: str, for_time: Optional[datetime.datetime] = None, valid_w

return utils.strings_equal(str(otp), str(self.at(for_time)))

def provisioning_uri(
self, name: Optional[str] = None, issuer_name: Optional[str] = None, image: Optional[str] = None
) -> str:

def provisioning_uri(self, name: Optional[str] = None, issuer_name: Optional[str] = None, **kwargs) -> str:
"""
Returns the provisioning URI for the OTP. This can then be
encoded in a QR Code and used to provision an OTP app like
Expand All @@ -103,7 +100,7 @@ def provisioning_uri(
algorithm=self.digest().name,
digits=self.digits,
period=self.interval,
image=image,
**kwargs,
)

def timecode(self, for_time: datetime.datetime) -> int:
Expand Down
17 changes: 10 additions & 7 deletions src/pyotp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def build_uri(
algorithm: Optional[str] = None,
digits: Optional[int] = None,
period: Optional[int] = None,
image: Optional[str] = None,
**kwargs,
) -> str:
"""
Returns the provisioning URI for the OTP; works for either TOTP or HOTP.
Expand All @@ -35,7 +35,7 @@ def build_uri(
:param digits: the length of the OTP generated code.
:param period: the number of seconds the OTP generator is set to
expire every code.
:param image: optional logo image url
:param kwargs: other query string parameters to include in the URI
:returns: provisioning uri
"""
# initial_count may be 0 as a valid param
Expand Down Expand Up @@ -64,11 +64,14 @@ def build_uri(
url_args["digits"] = digits
if is_period_set:
url_args["period"] = period
if image:
image_uri = urlparse(image)
if image_uri.scheme != "https" or not image_uri.netloc or not image_uri.path:
raise ValueError("{} is not a valid url".format(image_uri))
url_args["image"] = image
for k, v in kwargs.items():
if not isinstance(v, str):
raise ValueError("All otpauth uri parameters must be strings")
if k == "image":
image_uri = urlparse(v)
if image_uri.scheme != "https" or not image_uri.netloc or not image_uri.path:
raise ValueError("{} is not a valid url".format(image_uri))
url_args[k] = v

uri = base_uri.format(otp_type, label, urlencode(url_args).replace("+", "%20"))
return uri
Expand Down
13 changes: 7 additions & 6 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,6 @@ def test_invalids(self):
pyotp.parse_uri("otpauth://derp?secret=foo")
self.assertEqual("Not a supported OTP type", str(cm.exception))

with self.assertRaises(ValueError) as cm:
pyotp.parse_uri("otpauth://totp?foo=secret")
self.assertEqual("foo is not a valid parameter", str(cm.exception))

with self.assertRaises(ValueError) as cm:
pyotp.parse_uri("otpauth://totp?digits=-1")
self.assertEqual("Digits may only be 6, 7, or 8", str(cm.exception))
Expand All @@ -364,7 +360,7 @@ def test_invalids(self):
with self.assertRaises(ValueError) as cm:
pyotp.parse_uri("otpauth://totp?algorithm=aes")
self.assertEqual("Invalid value for algorithm, must be SHA1, SHA256 or SHA512", str(cm.exception))

def test_parse_steam(self):
otp = pyotp.parse_uri("otpauth://totp/Steam:?secret=SOME_SECRET&encoder=steam")
self.assertEqual(type(otp), pyotp.contrib.Steam)
Expand Down Expand Up @@ -435,13 +431,18 @@ def test_algorithms(self):
self.assertEqual(otp.at(90), "JG3T3")

# period and digits should be ignored
otp = pyotp.parse_uri("otpauth://totp/Steam:?secret=FMXNK4QEGKVPULRTADY6JIDK5VHUBGZW&period=15&digits=7&encoder=steam")
otp = pyotp.parse_uri(
"otpauth://totp/Steam:?secret=FMXNK4QEGKVPULRTADY6JIDK5VHUBGZW&period=15&digits=7&encoder=steam"
)
self.assertEqual(type(otp), pyotp.contrib.Steam)
self.assertEqual(otp.at(0), "C5V56")
self.assertEqual(otp.at(30), "QJY8Y")
self.assertEqual(otp.at(60), "R3WQY")
self.assertEqual(otp.at(90), "JG3T3")

pyotp.parse_uri("otpauth://totp?secret=abc&image=foobar")


class Timecop(object):
"""
Half-assed clone of timecop.rb, just enough to pass our tests.
Expand Down

0 comments on commit 249674b

Please sign in to comment.