| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455 |
- """Validation for API keys."""
- from __future__ import annotations
- import re
- # Matches a JWT: three non-empty base64url segments separated by dots.
- _JWT_RE = re.compile(
- r"""
- [\w-]+\. # header
- [\w-]+\. # payload
- [\w-]+ # signature
- """,
- re.VERBOSE,
- )
- def check_api_key(key: str) -> str | None:
- """Returns text describing problems with the API key, or None.
- If the key is in a valid format, returns None. Otherwise, returns
- a string formatted as a complete sentence (capitalized, punctuated)
- explaining the problem with the key.
- Args:
- key: The API key to check.
- """
- if not key:
- return "API key is empty."
- # Internal client JWTs have 3 dot-separated base64url segments
- # (header.payload.signature). They bypass legacy API key validation
- # and are sent via BasicAuth so the server can detect the JWT format.
- if _JWT_RE.fullmatch(key):
- return None
- # On-prem API keys have a variable-length prefix followed by a dash.
- #
- # NOTE: This should be rsplit(), but it is split() to be backward compatible
- # with tests that rely on that. It should be safe to change to rsplit()
- # once our tests are updated.
- parts = key.split("-", 1)
- if len(parts) == 1:
- secret = parts[0]
- else:
- _, secret = parts
- # NOTE: Dashes only allowed because of split() instead of rsplit() above.
- if not re.fullmatch(r"[\w-]+", secret):
- return "API key may only contain the letters A-Z, digits and underscores."
- if (secret_len := len(secret)) < 40:
- return f"API key must have 40+ characters, has {secret_len}."
- return None
|