Skip to content

Instantly share code, notes, and snippets.

@davidhariri
Created October 12, 2019 18:14
Show Gist options
  • Save davidhariri/b053787aabc9a8a9cc0893244e1549fe to your computer and use it in GitHub Desktop.
Save davidhariri/b053787aabc9a8a9cc0893244e1549fe to your computer and use it in GitHub Desktop.
Code required to verify Sign in with app-made Apple JWT tokens server-side in Python
import jwt
from jwt.algorithms import RSAAlgorithm
import requests
from time import time
import json
import os
APPLE_PUBLIC_KEY_URL = "https://appleid.apple.com/auth/keys"
APPLE_PUBLIC_KEY = None
APPLE_KEY_CACHE_EXP = 60 * 60 * 24
APPLE_LAST_KEY_FETCH = 0
class AppleUser(object):
def __init__(self, apple_id, email=None):
self.id = apple_id
self.email = email
self.full_user = False
if email is not None:
self.full_user = True
def __repr__(self):
return "<AppleUser {}>".format(self.id)
def _fetch_apple_public_key():
# Check to see if the public key is unset or is stale before returning
global APPLE_LAST_KEY_FETCH
global APPLE_PUBLIC_KEY
if (APPLE_LAST_KEY_FETCH + APPLE_KEY_CACHE_EXP) < int(time()) or APPLE_PUBLIC_KEY is None:
key_payload = requests.get(APPLE_PUBLIC_KEY_URL).json()
APPLE_PUBLIC_KEY = RSAAlgorithm.from_jwk(json.dumps(key_payload["keys"][0]))
APPLE_LAST_KEY_FETCH = int(time())
return APPLE_PUBLIC_KEY
def _decode_apple_user_token(apple_user_token):
public_key = _fetch_apple_public_key()
try:
token = jwt.decode(apple_user_token, public_key, audience=os.getenv("APPLE_APP_ID"), algorithm="RS256")
except jwt.exceptions.ExpiredSignatureError as e:
raise Exception("That token has expired")
except jwt.exceptions.InvalidAudienceError as e:
raise Exception("That token's audience did not match")
except Exception as e:
print(e)
raise Exception("An unexpected error occoured")
return token
def retrieve_user(user_token):
token = _decode_apple_user_token(user_token)
apple_user = AppleUser(token["sub"], token.get("email", None))
return apple_user
@Jan-Jasek
Copy link

Thank you, was very helpful. This is what I used with python-jose[cryptography] instead of old PyJWT

import logging
import requests

from typing import Dict, Union, List
from fastapi import HTTPException
from jose import jwt, jwk, JWTError
from starlette import status

from modules.database.models.member_user import MemberUser
from services.user_service.domain.providers.apple.constants import AppleUtilConstants
from settings.secrets import APPLE_OAUTH2_WEB_CLIENT_ID

def validate_user(provider_id_token_jwt: str) -> Dict[str, str]:
    # Get public key set from Apple
    try:
        keys: Dict[str, List[Dict[str, str]]] = dict(requests.get(AppleUtilConstants.KEYS_URL.value).json())
    except Exception as error:
        logging.exception(repr(error))
        raise Exception("Unable to connect to Apple.")
    # Get headers of our id token
    headers = jwt.get_unverified_headers(token=provider_id_token_jwt)
    # Find matching public key based on the kid header
    public_key: Dict[str, str] = next(filter(lambda key: (key["kid"] == headers["kid"]), keys["keys"]))
    # Construct the actual public_key
    public_rsa_key = jwk.construct(public_key_set)
    try:
        decoded_id_token = dict(
            jwt.decode(
                token=provider_id_token_jwt,
                audience=APPLE_OAUTH2_WEB_CLIENT_ID,
                key=public_key,
                algorithms=["RS256"],
            )
        )
    except jwt.ExpiredSignatureError as error:
        logging.info(repr(error))
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Token has expired",
        )
    except JWTError as error:
        logging.info(repr(error))
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Unable to process given id_token",
        )
    except Exception as error:
        logging.exception(repr(error))
        raise Exception("An unexpected error occurred")

    return decoded_id_token

@nikiforoveee
Copy link

thank you, so match ! I loosed 2 days before found this solution !

@adlrwbr
Copy link

adlrwbr commented Jun 7, 2022

This thread is a godsend

@pythonwood
Copy link

pyjwt >= 2.0 diff from pyjwt == 1.7.1 options={"verify_exp": False}, algorithms=["RS256"],

change log

.. code:: python

import jwt
from jwt import PyJWKClient

token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA"
kid = "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw"
url = "https://dev-87evx9ru.auth0.com/.well-known/jwks.json"

jwks_client = PyJWKClient(url)
signing_key = jwks_client.get_signing_key_from_jwt(token)

data = jwt.decode(
    token,
    signing_key.key,
    algorithms=["RS256"],
    audience="https://expenses-api",
    options={"verify_exp": False},
)
print(data)

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