Skip to content

Make Steam.provisioning_uri() round-trip through parse_uri() - #193

Open
dylanpulver wants to merge 1 commit into
pyauth:mainfrom
dylanpulver:fix-steam-provisioning-uri-roundtrip
Open

dylanpulver wants to merge 1 commit into
pyauth:mainfrom
dylanpulver:fix-steam-provisioning-uri-roundtrip

Conversation

@dylanpulver

Copy link
Copy Markdown

pyotp cannot parse the provisioning URI it generates for a Steam instance:

>>> pyotp.parse_uri(pyotp.contrib.Steam(secret).provisioning_uri())
ValueError: Digits may only be 6, 7, or 8

Reproduces on released 2.10.0 and at HEAD (cc15683).

Mechanism. Steam sets self.digits = 10 (contrib/steam.py:25) and doesn't override
provisioning_uri, so it inherits TOTP.provisioning_uri (totp.py:117), which passes
digits=self.digits to build_uri; utils.py:46 emits any digits != 6. The URI therefore
carries digits=10 and no encoder=steam, and parse_uri (__init__.py:100-102) rejects
digits outside {6,7,8} unless encoder=steam is present.

Both halves are wrong. The Key Uri Format spec both docstrings cite says "the digits parameter
may have the values 6 or 8". And without encoder=steam the URI doesn't identify itself as
Steam, so a consumer tolerating digits=10 yields ordinary TOTP codes (929991 vs C5V56
at t=0).

Fix. Override provisioning_uri on Steam to emit encoder=steam and omit the internal
digits. parse_uri is untouched; nothing previously rejected is now accepted.

Verification. make test/make lint/ruff format --check on 3.14: 28 passed before, 29
after; mypy clean. Adds the provisioning_uri → parse_uri → provisioning_uri round-trip
assertion the suite already makes for HOTP and TOTP in 10 places but never for Steam. Mutants:
reverting the fix fails the test; emitting encoder=steam and digits=10 fails only the
query-parameter assertion (it survives the round-trip check, as parse_uri skips the digit
check when encoder=steam); dropping digits without encoder=steam returns a TOTP.

Not tested. Only 3.14 locally, not the 3.10–3.14 matrix; no real Steam client scanned the
URI. Steam.__init__'s interval/digits args are separately ignored (steam.py:24 is
overwritten by TOTP.__init__) — left alone.

Written with AI assistance (Claude).

Steam inherits TOTP.provisioning_uri(), which emits digits=self.digits.
Steam sets self.digits = 10 (the Steam code is derived from a 10-digit HOTP
value), so the URI carries digits=10 and no encoder=steam marker. parse_uri()
rejects digits outside {6, 7, 8} unless encoder=steam is present, so the URI
pyotp generates for a Steam instance cannot be parsed back by pyotp:

    >>> pyotp.parse_uri(pyotp.contrib.Steam(secret).provisioning_uri())
    ValueError: Digits may only be 6, 7, or 8

Override provisioning_uri() on Steam to emit encoder=steam and omit the
internal digits value. Adds the provisioning_uri -> parse_uri -> provisioning_uri
round-trip assertion that the suite already makes for HOTP and TOTP.
@feiiiiii5

This comment was marked as low quality.

@kislyuk

kislyuk commented Oct 12, 2026

Copy link
Copy Markdown
Member

Hmm if this was not tested with a real client, what good is this change? Can anyone verify that this actually works with any authenticator app?

@dylanpulver

Copy link
Copy Markdown
Author

Fair question. No authenticator app can produce Steam codes from any otpauth:// URI, before or after this change. The Steam alphabet is not part of the Key Uri Format, so there is nothing there for a generic client to implement. encoder=steam is this repo's own convention, added to parse_uri() in #153 and accepted there since.

So a real-client test is not a bar this change can meet, and the current behaviour does not meet it either:

# today
otpauth://totp/alice?secret=FMXNK4QEGKVPULRTADY6JIDK5VHUBGZW&digits=10
  pyotp.parse_uri(...)              ValueError: Digits may only be 6, 7, or 8
  a client tolerating digits=10     0298929991
# with this PR
otpauth://totp/alice?secret=FMXNK4QEGKVPULRTADY6JIDK5VHUBGZW&encoder=steam
  pyotp.parse_uri(...)              Steam, at(0) == C5V56, matches the generator
  a client ignoring encoder=        929991

Neither URI gets a Steam code out of a generic app, and digits=10 is not a legal value for that parameter, so the current URI is invalid as well as unparseable by this library.

What the change fixes is pyotp parsing its own output, which the suite asserts for HOTP and TOTP in 10 places and never for Steam.

If you would rather Steam.provisioning_uri() raise instead of emitting a URI no third-party app can act on, that is a defensible position and I will switch it to that.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants