Created
September 24, 2024 17:29
-
-
Save montasaurus/6376b15f334d93262190a3de50fd0716 to your computer and use it in GitHub Desktop.
Parse and validate a Sign In with Apple ID JWT token in Python. Works as a CLI to test tokens with `uv run appleid_auth.py`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# /// script | |
# dependencies = [ | |
# "httpx", | |
# "pyjwt[crypto]", | |
# ] | |
# /// | |
from dataclasses import dataclass | |
from datetime import timedelta | |
from typing import Any | |
import httpx | |
import jwt | |
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey | |
from jwt.algorithms import RSAAlgorithm | |
APPLE_PUBLIC_KEY_URL = "https://appleid.apple.com/auth/keys" | |
APPLE_ISSUER = "https://appleid.apple.com" | |
@dataclass | |
class ApplePublicKey: | |
key_id: str | |
public_key: RSAPublicKey | |
algorithm: str | |
# You could cache this, invalidating when you see a key id you don't recognize | |
def fetch_apple_public_keys() -> dict[str, ApplePublicKey]: | |
key_payload = httpx.get(APPLE_PUBLIC_KEY_URL).json() | |
keys = key_payload["keys"] | |
key_dict = { | |
key["kid"]: ApplePublicKey( | |
key_id=key["kid"], | |
public_key=RSAAlgorithm.from_jwk(key), # type: ignore | |
algorithm=key["alg"], | |
) | |
for key in keys | |
} | |
return key_dict | |
def decode_apple_user_token( | |
*, | |
apple_user_token: str, | |
apple_app_id: str | None, | |
options: dict[str, Any] | None = None, | |
): | |
if apple_app_id is None: | |
options = options or {} | |
options["verify_aud"] = False | |
public_keys = fetch_apple_public_keys() | |
unverified_header = jwt.get_unverified_header(apple_user_token) | |
key_id = unverified_header["kid"] | |
apple_public_key = public_keys.get(key_id) | |
if apple_public_key is None: | |
raise ValueError( | |
"Can't find an Apple public key for the key ID provided in the token header" | |
) | |
# This will throw exceptions e.g. jwt.exceptions.ExpiredSignatureError and jwt.exceptions.InvalidAudienceError | |
token = jwt.decode( | |
jwt=apple_user_token, | |
key=apple_public_key.public_key, | |
audience=apple_app_id, | |
issuer=APPLE_ISSUER, | |
algorithms=[apple_public_key.algorithm], | |
leeway=timedelta(minutes=5), | |
options=options, | |
) | |
return token | |
if __name__ == "__main__": | |
apple_user_token = input("User's identity token JWT: ") | |
apple_app_id = ( | |
input( | |
"Your Apple app ID (e.g. com.example.app) [leave blank to skip verification]: " | |
) | |
or None | |
) | |
verify_timestamps = input("Verify timestamps? [Y/n]: ") or "Y" | |
options = None | |
if verify_timestamps.upper() == "N": | |
options = {"verify_exp": False, "verify_iat": False} | |
decoded_token = decode_apple_user_token( | |
apple_user_token=apple_user_token, | |
apple_app_id=apple_app_id, | |
options=options, | |
) | |
print() # noqa: T201 | |
print(f"Decoded token with user id {decoded_token['sub']}\n") # noqa: T201 | |
print(decoded_token) # noqa: T201 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment