validation.py 1.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. """Validation for API keys."""
  2. from __future__ import annotations
  3. import re
  4. # Matches a JWT: three non-empty base64url segments separated by dots.
  5. _JWT_RE = re.compile(
  6. r"""
  7. [\w-]+\. # header
  8. [\w-]+\. # payload
  9. [\w-]+ # signature
  10. """,
  11. re.VERBOSE,
  12. )
  13. def check_api_key(key: str) -> str | None:
  14. """Returns text describing problems with the API key, or None.
  15. If the key is in a valid format, returns None. Otherwise, returns
  16. a string formatted as a complete sentence (capitalized, punctuated)
  17. explaining the problem with the key.
  18. Args:
  19. key: The API key to check.
  20. """
  21. if not key:
  22. return "API key is empty."
  23. # Internal client JWTs have 3 dot-separated base64url segments
  24. # (header.payload.signature). They bypass legacy API key validation
  25. # and are sent via BasicAuth so the server can detect the JWT format.
  26. if _JWT_RE.fullmatch(key):
  27. return None
  28. # On-prem API keys have a variable-length prefix followed by a dash.
  29. #
  30. # NOTE: This should be rsplit(), but it is split() to be backward compatible
  31. # with tests that rely on that. It should be safe to change to rsplit()
  32. # once our tests are updated.
  33. parts = key.split("-", 1)
  34. if len(parts) == 1:
  35. secret = parts[0]
  36. else:
  37. _, secret = parts
  38. # NOTE: Dashes only allowed because of split() instead of rsplit() above.
  39. if not re.fullmatch(r"[\w-]+", secret):
  40. return "API key may only contain the letters A-Z, digits and underscores."
  41. if (secret_len := len(secret)) < 40:
  42. return f"API key must have 40+ characters, has {secret_len}."
  43. return None