From 11cefe14ef5a6a08958fb5323d8cc224bb783a95 Mon Sep 17 00:00:00 2001 From: Erik Gaasedelen Date: Sun, 14 Dec 2025 13:00:57 -0800 Subject: [PATCH 1/4] add apple sign in --- fasthtml/_modidx.py | 7 +- fasthtml/oauth.py | 29 ++++- nbs/api/00_core.ipynb | 254 ++++++++++++++++++++--------------------- nbs/api/08_oauth.ipynb | 65 ++++++++++- 4 files changed, 216 insertions(+), 139 deletions(-) diff --git a/fasthtml/_modidx.py b/fasthtml/_modidx.py index df728d88..a37a7ca5 100644 --- a/fasthtml/_modidx.py +++ b/fasthtml/_modidx.py @@ -158,7 +158,12 @@ 'fasthtml.jupyter.wait_port_free': ('api/jupyter.html#wait_port_free', 'fasthtml/jupyter.py'), 'fasthtml.jupyter.ws_client': ('api/jupyter.html#ws_client', 'fasthtml/jupyter.py')}, 'fasthtml.live_reload': {}, - 'fasthtml.oauth': { 'fasthtml.oauth.Auth0AppClient': ('api/oauth.html#auth0appclient', 'fasthtml/oauth.py'), + 'fasthtml.oauth': { 'fasthtml.oauth.AppleAppClient': ('api/oauth.html#appleappclient', 'fasthtml/oauth.py'), + 'fasthtml.oauth.AppleAppClient.__init__': ('api/oauth.html#appleappclient.__init__', 'fasthtml/oauth.py'), + 'fasthtml.oauth.AppleAppClient.client_secret': ( 'api/oauth.html#appleappclient.client_secret', + 'fasthtml/oauth.py'), + 'fasthtml.oauth.AppleAppClient.get_info': ('api/oauth.html#appleappclient.get_info', 'fasthtml/oauth.py'), + 'fasthtml.oauth.Auth0AppClient': ('api/oauth.html#auth0appclient', 'fasthtml/oauth.py'), 'fasthtml.oauth.Auth0AppClient.__init__': ('api/oauth.html#auth0appclient.__init__', 'fasthtml/oauth.py'), 'fasthtml.oauth.Auth0AppClient._fetch_openid_config': ( 'api/oauth.html#auth0appclient._fetch_openid_config', 'fasthtml/oauth.py'), diff --git a/fasthtml/oauth.py b/fasthtml/oauth.py index 76e96917..be2dce37 100644 --- a/fasthtml/oauth.py +++ b/fasthtml/oauth.py @@ -4,7 +4,7 @@ # %% auto 0 __all__ = ['http_patterns', 'GoogleAppClient', 'GitHubAppClient', 'HuggingFaceClient', 'DiscordAppClient', 'Auth0AppClient', - 'get_host', 'redir_url', 'url_match', 'OAuth', 'load_creds'] + 'AppleAppClient', 'get_host', 'redir_url', 'url_match', 'OAuth', 'load_creds'] # %% ../nbs/api/08_oauth.ipynb from .common import * @@ -112,6 +112,28 @@ def login_link(self, req): d = dict(response_type="code", client_id=self.client_id, scope=self.scope, redirect_uri=redir_url(req, self.redirect_uri)) return f"{self.base_url}?{urlencode(d)}" +# %% ../nbs/api/08_oauth.ipynb +class AppleAppClient(_AppClient): + "A `WebApplicationClient` for Apple Sign In" + base_url = "https://appleid.apple.com/auth/authorize" + token_url = "https://appleid.apple.com/auth/token" + + def __init__(self, client_id, key_id, team_id, private_key, code=None, scope=None, **kwargs): + if not scope: scope = ["name", "email"] + super().__init__(client_id, client_secret=None, code=code, scope=scope, **kwargs) + self.key_id, self.team_id, self.private_key = key_id, team_id, private_key + + @property + def client_secret(self): + now = int(time.time()) + payload = dict(iss=self.team_id, iat=now, exp=now + 86400 * 180, aud='https://appleid.apple.com', sub=self.client_id) + return jwt.encode(payload, self.private_key, algorithm='ES256', headers={'kid': self.key_id}) + + def get_info(self, token=None): + "Decode user info from the ID token" + if token: self.token = token + return jwt.decode(self.token.get('id_token'), options={"verify_signature": False}) + # %% ../nbs/api/08_oauth.ipynb @patch def login_link(self:WebApplicationClient, redirect_uri, scope=None, state=None, **kwargs): @@ -171,8 +193,9 @@ def url_match(request, patterns=http_patterns): # %% ../nbs/api/08_oauth.ipynb class OAuth: - def __init__(self, app, cli, skip=None, redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login', https=True, http_patterns=http_patterns): + def __init__(self, app, cli, skip=None, redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login', https=True, http_patterns=http_patterns, redir_method='get'): if not skip: skip = [redir_path,error_path,login_path] + redir_handler = app.post if redir_method == 'post' else app.get store_attr() def before(req, session): if 'auth' not in req.scope: req.scope['auth'] = session.get('auth') @@ -182,7 +205,7 @@ def before(req, session): if res: return res app.before.append(Beforeware(before, skip=skip)) - @app.get(redir_path) + @redir_handler(redir_path) def redirect(req, session, code:str=None, error:str=None, state:str=None): if not code: session['oauth_error']=error diff --git a/nbs/api/00_core.ipynb b/nbs/api/00_core.ipynb index 25b039f2..2932b473 100644 --- a/nbs/api/00_core.ipynb +++ b/nbs/api/00_core.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "fa505c58", "metadata": {}, "outputs": [], @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "23503b9e", "metadata": {}, "outputs": [], @@ -70,7 +70,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "7f5d0a72", "metadata": {}, "outputs": [], @@ -89,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "19d3f2a7", "metadata": {}, "outputs": [], @@ -110,7 +110,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "e68a76c9", "metadata": {}, "outputs": [], @@ -123,7 +123,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "5331a3e7", "metadata": {}, "outputs": [ @@ -133,7 +133,7 @@ "datetime.datetime(2025, 11, 19, 14, 0)" ] }, - "execution_count": 6, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -144,7 +144,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "c40c9071", "metadata": {}, "outputs": [ @@ -154,7 +154,7 @@ "True" ] }, - "execution_count": 7, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -165,7 +165,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "7c820373", "metadata": {}, "outputs": [], @@ -179,7 +179,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "442a5aac", "metadata": {}, "outputs": [ @@ -189,7 +189,7 @@ "'Snake-Case'" ] }, - "execution_count": 9, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -200,7 +200,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "25f3b8f8", "metadata": {}, "outputs": [], @@ -229,7 +229,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "5b4b5d95", "metadata": {}, "outputs": [], @@ -249,7 +249,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "36e2cac0", "metadata": {}, "outputs": [ @@ -259,7 +259,7 @@ "HtmxHeaders(boosted=None, current_url=None, history_restore_request=None, prompt=None, request='1', target=None, trigger_name=None, trigger=None)" ] }, - "execution_count": 12, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -271,7 +271,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "1d53e8e7", "metadata": {}, "outputs": [], @@ -290,7 +290,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "5ab74473", "metadata": {}, "outputs": [], @@ -301,7 +301,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "0afb520c", "metadata": {}, "outputs": [], @@ -321,7 +321,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "95b1f5c9", "metadata": {}, "outputs": [], @@ -336,7 +336,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "c58ccadb", "metadata": {}, "outputs": [], @@ -354,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "id": "59757d76", "metadata": {}, "outputs": [], @@ -367,7 +367,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "5fc04751", "metadata": {}, "outputs": [], @@ -379,7 +379,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "94e18161", "metadata": {}, "outputs": [], @@ -393,7 +393,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "id": "592d6c8e", "metadata": {}, "outputs": [ @@ -403,7 +403,7 @@ "'HX-Trigger-After-Settle'" ] }, - "execution_count": 21, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -414,7 +414,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "id": "f6a2e62e", "metadata": {}, "outputs": [], @@ -429,7 +429,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "id": "e89857eb", "metadata": {}, "outputs": [ @@ -439,7 +439,7 @@ "HttpHeader(k='HX-Trigger-After-Settle', v='hi')" ] }, - "execution_count": 23, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -450,7 +450,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "id": "56cc589f", "metadata": {}, "outputs": [], @@ -464,7 +464,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "id": "5453a684", "metadata": {}, "outputs": [ @@ -474,7 +474,7 @@ "{'a': int, 'b': str}" ] }, - "execution_count": 25, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -487,7 +487,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "bcad3a5a", "metadata": {}, "outputs": [ @@ -497,7 +497,7 @@ "{'x': str, 'y': str}" ] }, - "execution_count": 26, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -509,7 +509,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "id": "cb4ed4aa", "metadata": {}, "outputs": [], @@ -520,7 +520,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "id": "ffdde66f", "metadata": {}, "outputs": [], @@ -535,7 +535,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "id": "5fe74444", "metadata": {}, "outputs": [], @@ -549,7 +549,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "id": "cf80ea34", "metadata": {}, "outputs": [], @@ -563,7 +563,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "42c9cea0", "metadata": {}, "outputs": [], @@ -584,7 +584,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "id": "33d3bc16", "metadata": {}, "outputs": [], @@ -612,7 +612,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "id": "8235f17f", "metadata": {}, "outputs": [], @@ -626,7 +626,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "id": "a4873835", "metadata": {}, "outputs": [], @@ -640,7 +640,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "id": "4b36f60b", "metadata": {}, "outputs": [ @@ -650,7 +650,7 @@ "Foo(d={'a': 1})" ] }, - "execution_count": 35, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -661,7 +661,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": null, "id": "7cc39ba9", "metadata": {}, "outputs": [], @@ -676,7 +676,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "id": "2a8b10f4", "metadata": {}, "outputs": [ @@ -704,7 +704,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": null, "id": "9246153f", "metadata": {}, "outputs": [ @@ -714,7 +714,7 @@ "\"['1', '2']\"" ] }, - "execution_count": 38, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -727,7 +727,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": null, "id": "6775cbf8", "metadata": {}, "outputs": [], @@ -778,7 +778,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": null, "id": "bf945ee8", "metadata": {}, "outputs": [ @@ -786,7 +786,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'req': , 'this': , 'a': '1', 'b': HttpHeader(k='value1', v='value3')}\n" + "{'req': , 'this': , 'a': '1', 'b': HttpHeader(k='value1', v='value3')}\n" ] } ], @@ -804,7 +804,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": null, "id": "a3ded5ec", "metadata": {}, "outputs": [ @@ -812,7 +812,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'req': , 'this': , 'a': '1', 'b': HttpHeader(k='value1', v='value3')}\n" + "{'req': , 'this': , 'a': '1', 'b': HttpHeader(k='value1', v='value3')}\n" ] } ], @@ -846,7 +846,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": null, "id": "c369a89c", "metadata": {}, "outputs": [ @@ -854,7 +854,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'req': , 'this': , 'a': ''}\n" + "{'req': , 'this': , 'a': ''}\n" ] } ], @@ -880,7 +880,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": null, "id": "b33b43a7", "metadata": {}, "outputs": [ @@ -906,7 +906,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": null, "id": "7a661bfa", "metadata": {}, "outputs": [], @@ -924,7 +924,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": null, "id": "2ee5adf1", "metadata": {}, "outputs": [], @@ -936,7 +936,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": null, "id": "aacff5ac", "metadata": {}, "outputs": [], @@ -948,7 +948,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": null, "id": "78c3c357", "metadata": {}, "outputs": [], @@ -968,7 +968,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": null, "id": "f2277c02", "metadata": {}, "outputs": [], @@ -1005,7 +1005,7 @@ }, { "cell_type": "code", - "execution_count": 49, + "execution_count": null, "id": "dcc15129", "metadata": {}, "outputs": [], @@ -1039,7 +1039,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": null, "id": "983bcfe2", "metadata": {}, "outputs": [], @@ -1055,7 +1055,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": null, "id": "5b0e7677", "metadata": {}, "outputs": [], @@ -1068,7 +1068,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": null, "id": "0dd0a414", "metadata": {}, "outputs": [], @@ -1095,7 +1095,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": null, "id": "03f4c639", "metadata": {}, "outputs": [], @@ -1108,7 +1108,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": null, "id": "bdb2ac14", "metadata": {}, "outputs": [], @@ -1122,7 +1122,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": null, "id": "b80ce139", "metadata": {}, "outputs": [], @@ -1133,7 +1133,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": null, "id": "a27adc1e", "metadata": {}, "outputs": [], @@ -1151,7 +1151,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": null, "id": "46614165", "metadata": {}, "outputs": [], @@ -1165,7 +1165,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": null, "id": "c1707d59", "metadata": {}, "outputs": [], @@ -1207,7 +1207,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": null, "id": "f1e3ed2d", "metadata": {}, "outputs": [], @@ -1218,7 +1218,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": null, "id": "a407dc0e", "metadata": {}, "outputs": [], @@ -1237,7 +1237,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": null, "id": "d36402e6", "metadata": {}, "outputs": [], @@ -1250,7 +1250,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": null, "id": "7f49728d", "metadata": {}, "outputs": [], @@ -1276,7 +1276,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": null, "id": "b2007479", "metadata": {}, "outputs": [], @@ -1290,7 +1290,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": null, "id": "358badec", "metadata": {}, "outputs": [], @@ -1315,7 +1315,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": null, "id": "da449a7b", "metadata": {}, "outputs": [], @@ -1339,7 +1339,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": null, "id": "775fb66b", "metadata": {}, "outputs": [], @@ -1352,7 +1352,7 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": null, "id": "968d9245", "metadata": {}, "outputs": [], @@ -1380,7 +1380,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": null, "id": "eac44461", "metadata": {}, "outputs": [], @@ -1396,7 +1396,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": null, "id": "1ba52822", "metadata": {}, "outputs": [], @@ -1410,7 +1410,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": null, "id": "b0d1cbbf", "metadata": {}, "outputs": [], @@ -1441,7 +1441,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": null, "id": "deffbaaa", "metadata": {}, "outputs": [ @@ -1459,7 +1459,7 @@ }, { "cell_type": "code", - "execution_count": 72, + "execution_count": null, "id": "60cb52ea", "metadata": {}, "outputs": [], @@ -1475,7 +1475,7 @@ }, { "cell_type": "code", - "execution_count": 73, + "execution_count": null, "id": "8bd78eeb", "metadata": {}, "outputs": [], @@ -1493,7 +1493,7 @@ }, { "cell_type": "code", - "execution_count": 74, + "execution_count": null, "id": "042666e2", "metadata": {}, "outputs": [], @@ -1506,7 +1506,7 @@ }, { "cell_type": "code", - "execution_count": 75, + "execution_count": null, "id": "d276fc71", "metadata": {}, "outputs": [], @@ -1524,7 +1524,7 @@ }, { "cell_type": "code", - "execution_count": 76, + "execution_count": null, "id": "bc323fd4", "metadata": {}, "outputs": [], @@ -1552,7 +1552,7 @@ }, { "cell_type": "code", - "execution_count": 77, + "execution_count": null, "id": "ff82dc78", "metadata": {}, "outputs": [], @@ -1562,7 +1562,7 @@ }, { "cell_type": "code", - "execution_count": 78, + "execution_count": null, "id": "c0836a8f", "metadata": {}, "outputs": [], @@ -1581,7 +1581,7 @@ }, { "cell_type": "code", - "execution_count": 79, + "execution_count": null, "id": "50ebb1ee", "metadata": {}, "outputs": [], @@ -1593,7 +1593,7 @@ }, { "cell_type": "code", - "execution_count": 80, + "execution_count": null, "id": "f86690c4", "metadata": {}, "outputs": [], @@ -1609,7 +1609,7 @@ }, { "cell_type": "code", - "execution_count": 81, + "execution_count": null, "id": "2c5285ae", "metadata": {}, "outputs": [], @@ -1631,7 +1631,7 @@ }, { "cell_type": "code", - "execution_count": 82, + "execution_count": null, "id": "3327a1e9", "metadata": {}, "outputs": [], @@ -1675,7 +1675,7 @@ }, { "cell_type": "code", - "execution_count": 83, + "execution_count": null, "id": "e0accf76", "metadata": {}, "outputs": [], @@ -1693,7 +1693,7 @@ }, { "cell_type": "code", - "execution_count": 84, + "execution_count": null, "id": "246bd8d1", "metadata": {}, "outputs": [], @@ -1704,7 +1704,7 @@ }, { "cell_type": "code", - "execution_count": 85, + "execution_count": null, "id": "26b147ba", "metadata": {}, "outputs": [], @@ -1738,7 +1738,7 @@ }, { "cell_type": "code", - "execution_count": 86, + "execution_count": null, "id": "3818575c", "metadata": {}, "outputs": [], @@ -1756,7 +1756,7 @@ }, { "cell_type": "code", - "execution_count": 87, + "execution_count": null, "id": "669e76eb", "metadata": {}, "outputs": [], @@ -1771,7 +1771,7 @@ }, { "cell_type": "code", - "execution_count": 88, + "execution_count": null, "id": "919618c3", "metadata": {}, "outputs": [], @@ -1793,7 +1793,7 @@ }, { "cell_type": "code", - "execution_count": 89, + "execution_count": null, "id": "d2ecc738", "metadata": {}, "outputs": [], @@ -1806,7 +1806,7 @@ }, { "cell_type": "code", - "execution_count": 90, + "execution_count": null, "id": "c0f13ece", "metadata": {}, "outputs": [], @@ -1818,7 +1818,7 @@ }, { "cell_type": "code", - "execution_count": 91, + "execution_count": null, "id": "b218f738", "metadata": {}, "outputs": [ @@ -1828,7 +1828,7 @@ "'f_g'" ] }, - "execution_count": 91, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1840,7 +1840,7 @@ }, { "cell_type": "code", - "execution_count": 92, + "execution_count": null, "id": "72760b09", "metadata": {}, "outputs": [], @@ -1864,7 +1864,7 @@ }, { "cell_type": "code", - "execution_count": 93, + "execution_count": null, "id": "f5cb2c2b", "metadata": {}, "outputs": [], @@ -1881,7 +1881,7 @@ }, { "cell_type": "code", - "execution_count": 94, + "execution_count": null, "id": "e6ee3a86", "metadata": {}, "outputs": [ @@ -1891,7 +1891,7 @@ "'/foo?a=bar&b=1&b=2'" ] }, - "execution_count": 94, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1906,7 +1906,7 @@ }, { "cell_type": "code", - "execution_count": 95, + "execution_count": null, "id": "9b9f1f03", "metadata": {}, "outputs": [ @@ -1916,7 +1916,7 @@ "'/foo/bar?b=1&b=2'" ] }, - "execution_count": 95, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -1930,7 +1930,7 @@ }, { "cell_type": "code", - "execution_count": 96, + "execution_count": null, "id": "35c35a96", "metadata": {}, "outputs": [], @@ -1945,7 +1945,7 @@ }, { "cell_type": "code", - "execution_count": 97, + "execution_count": null, "id": "3a348474", "metadata": {}, "outputs": [], @@ -1976,7 +1976,7 @@ }, { "cell_type": "code", - "execution_count": 98, + "execution_count": null, "id": "8121968a", "metadata": {}, "outputs": [], @@ -1996,7 +1996,7 @@ }, { "cell_type": "code", - "execution_count": 99, + "execution_count": null, "id": "b163c933", "metadata": {}, "outputs": [ @@ -2006,7 +2006,7 @@ "'test'" ] }, - "execution_count": 99, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -2036,7 +2036,7 @@ }, { "cell_type": "code", - "execution_count": 100, + "execution_count": null, "id": "9abc3781", "metadata": {}, "outputs": [], @@ -2046,7 +2046,7 @@ }, { "cell_type": "code", - "execution_count": 101, + "execution_count": null, "id": "645d8d95", "metadata": {}, "outputs": [], @@ -2056,7 +2056,7 @@ }, { "cell_type": "code", - "execution_count": 102, + "execution_count": null, "id": "421262a8", "metadata": {}, "outputs": [ @@ -2073,7 +2073,7 @@ "'/foo?param=value'" ] }, - "execution_count": 102, + "execution_count": null, "metadata": {}, "output_type": "execute_result" } @@ -3965,21 +3965,9 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "python3", "language": "python", "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.8" } }, "nbformat": 4, diff --git a/nbs/api/08_oauth.ipynb b/nbs/api/08_oauth.ipynb index e1010b31..0de74b92 100644 --- a/nbs/api/08_oauth.ipynb +++ b/nbs/api/08_oauth.ipynb @@ -213,6 +213,66 @@ " return f\"{self.base_url}?{urlencode(d)}\"" ] }, + { + "cell_type": "markdown", + "id": "b2643c29", + "metadata": {}, + "source": [ + "Apple Sign In requires a few extra steps compared to other OAuth providers:\n", + "\n", + "1. **Client secret is a JWT** — Instead of a static secret, you generate a signed JWT using a private key from Apple\n", + "2. **Callback is POST, not GET** — Apple sends the authorization code via form POST, so you need `redir_method='post'`\n", + "3. **User info is in the ID token** — There's no separate userinfo endpoint; you decode the JWT they return\n", + "\n", + "See [Apple's Sign In documentation](https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple) for details." + ] + }, + { + "cell_type": "markdown", + "id": "0957a0a0", + "metadata": {}, + "source": [ + "To use Apple Sign In, you'll need credentials from the [Apple Developer Portal](https://developer.apple.com/account):\n", + "\n", + "1. **Team ID** — Found in your Membership section\n", + "2. **App ID** — Create under Identifiers → App IDs with \"Sign in with Apple\" capability enabled\n", + "3. **Service ID** — Create under Identifiers → Services IDs, linked to your App ID (this becomes your `client_id`)\n", + "4. **Key ID** — Create under Keys with \"Sign in with Apple\" enabled\n", + "5. **Private Key (.p8 file)** — Downloaded when you create the key (you can only download it once!)\n", + "\n", + "When configuring your Service ID, you'll need to specify your domain and return URL (e.g., `https://yourdomain.com/auth/callback`). Note: Apple doesn't accept `localhost` — use a real domain or a tunneling service like ngrok for local development. (I used a solveit public domain for easy prototyping)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39425d0b", + "metadata": {}, + "outputs": [], + "source": [ + "#|export\n", + "class AppleAppClient(_AppClient):\n", + " \"A `WebApplicationClient` for Apple Sign In\"\n", + " base_url = \"https://appleid.apple.com/auth/authorize\"\n", + " token_url = \"https://appleid.apple.com/auth/token\"\n", + " \n", + " def __init__(self, client_id, key_id, team_id, private_key, code=None, scope=None, **kwargs):\n", + " if not scope: scope = [\"name\", \"email\"]\n", + " super().__init__(client_id, client_secret=None, code=code, scope=scope, **kwargs)\n", + " self.key_id, self.team_id, self.private_key = key_id, team_id, private_key\n", + " \n", + " @property\n", + " def client_secret(self):\n", + " now = int(time.time())\n", + " payload = dict(iss=self.team_id, iat=now, exp=now + 86400 * 180, aud='https://appleid.apple.com', sub=self.client_id)\n", + " return jwt.encode(payload, self.private_key, algorithm='ES256', headers={'kid': self.key_id})\n", + " \n", + " def get_info(self, token=None):\n", + " \"Decode user info from the ID token\"\n", + " if token: self.token = token\n", + " return jwt.decode(self.token.get('id_token'), options={\"verify_signature\": False})" + ] + }, { "cell_type": "code", "execution_count": null, @@ -531,8 +591,9 @@ "source": [ "#| export\n", "class OAuth:\n", - " def __init__(self, app, cli, skip=None, redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login', https=True, http_patterns=http_patterns):\n", + " def __init__(self, app, cli, skip=None, redir_path='/redirect', error_path='/error', logout_path='/logout', login_path='/login', https=True, http_patterns=http_patterns, redir_method='get'):\n", " if not skip: skip = [redir_path,error_path,login_path]\n", + " redir_handler = app.post if redir_method == 'post' else app.get\n", " store_attr()\n", " def before(req, session):\n", " if 'auth' not in req.scope: req.scope['auth'] = session.get('auth')\n", @@ -542,7 +603,7 @@ " if res: return res\n", " app.before.append(Beforeware(before, skip=skip))\n", "\n", - " @app.get(redir_path)\n", + " @redir_handler(redir_path)\n", " def redirect(req, session, code:str=None, error:str=None, state:str=None):\n", " if not code:\n", " session['oauth_error']=error\n", From 6eaf2fe95925fa3442b6b058dc38b6bc44cced3d Mon Sep 17 00:00:00 2001 From: Erik Gaasedelen Date: Sun, 14 Dec 2025 13:36:21 -0800 Subject: [PATCH 2/4] deps --- fasthtml/oauth.py | 2 +- nbs/api/08_oauth.ipynb | 2 +- settings.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fasthtml/oauth.py b/fasthtml/oauth.py index be2dce37..9f5b0816 100644 --- a/fasthtml/oauth.py +++ b/fasthtml/oauth.py @@ -10,7 +10,7 @@ from .common import * from oauthlib.oauth2 import WebApplicationClient from urllib.parse import urlparse, urlencode, parse_qs, quote, unquote -import secrets, httpx +import secrets, httpx, time, jwt # %% ../nbs/api/08_oauth.ipynb class _AppClient(WebApplicationClient): diff --git a/nbs/api/08_oauth.ipynb b/nbs/api/08_oauth.ipynb index 0de74b92..bab76ef7 100644 --- a/nbs/api/08_oauth.ipynb +++ b/nbs/api/08_oauth.ipynb @@ -41,7 +41,7 @@ "from fasthtml.common import *\n", "from oauthlib.oauth2 import WebApplicationClient\n", "from urllib.parse import urlparse, urlencode, parse_qs, quote, unquote\n", - "import secrets, httpx" + "import secrets, httpx, time, jwt" ] }, { diff --git a/settings.ini b/settings.ini index 9504a1b8..fc1bd274 100644 --- a/settings.ini +++ b/settings.ini @@ -4,7 +4,7 @@ lib_name = python-fasthtml version = 0.12.37 min_python = 3.10 license = apache2 -requirements = fastcore>=1.8.1 python-dateutil starlette>0.33 oauthlib itsdangerous uvicorn[standard]>=0.30 httpx fastlite>=0.1.1 python-multipart beautifulsoup4 +requirements = fastcore>=1.8.1 python-dateutil starlette>0.33 oauthlib itsdangerous uvicorn[standard]>=0.30 httpx fastlite>=0.1.1 python-multipart beautifulsoup4 PyJWT dev_requirements = ipython lxml pysymbol_llm monsterui black_formatting = False conda_user = fastai From dd2d39a1f25ca38044144bba5dc09979b1b61b80 Mon Sep 17 00:00:00 2001 From: Erik Gaasedelen Date: Sun, 14 Dec 2025 13:44:18 -0800 Subject: [PATCH 3/4] settr --- fasthtml/oauth.py | 3 +++ nbs/api/08_oauth.ipynb | 3 +++ 2 files changed, 6 insertions(+) diff --git a/fasthtml/oauth.py b/fasthtml/oauth.py index 9f5b0816..32988862 100644 --- a/fasthtml/oauth.py +++ b/fasthtml/oauth.py @@ -129,6 +129,9 @@ def client_secret(self): payload = dict(iss=self.team_id, iat=now, exp=now + 86400 * 180, aud='https://appleid.apple.com', sub=self.client_id) return jwt.encode(payload, self.private_key, algorithm='ES256', headers={'kid': self.key_id}) + @client_secret.setter + def client_secret(self, value): pass + def get_info(self, token=None): "Decode user info from the ID token" if token: self.token = token diff --git a/nbs/api/08_oauth.ipynb b/nbs/api/08_oauth.ipynb index bab76ef7..e6afc6ce 100644 --- a/nbs/api/08_oauth.ipynb +++ b/nbs/api/08_oauth.ipynb @@ -267,6 +267,9 @@ " payload = dict(iss=self.team_id, iat=now, exp=now + 86400 * 180, aud='https://appleid.apple.com', sub=self.client_id)\n", " return jwt.encode(payload, self.private_key, algorithm='ES256', headers={'kid': self.key_id})\n", " \n", + " @client_secret.setter\n", + " def client_secret(self, value): pass\n", + " \n", " def get_info(self, token=None):\n", " \"Decode user info from the ID token\"\n", " if token: self.token = token\n", From ab017e65d13d1787de6ba88feb53410962e80c82 Mon Sep 17 00:00:00 2001 From: Erik Gaasedelen Date: Sun, 14 Dec 2025 14:32:01 -0800 Subject: [PATCH 4/4] jwt dev dep --- fasthtml/oauth.py | 4 +++- nbs/api/08_oauth.ipynb | 4 +++- settings.ini | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/fasthtml/oauth.py b/fasthtml/oauth.py index 32988862..2f50e22a 100644 --- a/fasthtml/oauth.py +++ b/fasthtml/oauth.py @@ -10,7 +10,7 @@ from .common import * from oauthlib.oauth2 import WebApplicationClient from urllib.parse import urlparse, urlencode, parse_qs, quote, unquote -import secrets, httpx, time, jwt +import secrets, httpx, time # %% ../nbs/api/08_oauth.ipynb class _AppClient(WebApplicationClient): @@ -125,6 +125,7 @@ def __init__(self, client_id, key_id, team_id, private_key, code=None, scope=Non @property def client_secret(self): + import jwt now = int(time.time()) payload = dict(iss=self.team_id, iat=now, exp=now + 86400 * 180, aud='https://appleid.apple.com', sub=self.client_id) return jwt.encode(payload, self.private_key, algorithm='ES256', headers={'kid': self.key_id}) @@ -134,6 +135,7 @@ def client_secret(self, value): pass def get_info(self, token=None): "Decode user info from the ID token" + import jwt if token: self.token = token return jwt.decode(self.token.get('id_token'), options={"verify_signature": False}) diff --git a/nbs/api/08_oauth.ipynb b/nbs/api/08_oauth.ipynb index e6afc6ce..222d1ddf 100644 --- a/nbs/api/08_oauth.ipynb +++ b/nbs/api/08_oauth.ipynb @@ -41,7 +41,7 @@ "from fasthtml.common import *\n", "from oauthlib.oauth2 import WebApplicationClient\n", "from urllib.parse import urlparse, urlencode, parse_qs, quote, unquote\n", - "import secrets, httpx, time, jwt" + "import secrets, httpx, time" ] }, { @@ -263,6 +263,7 @@ " \n", " @property\n", " def client_secret(self):\n", + " import jwt\n", " now = int(time.time())\n", " payload = dict(iss=self.team_id, iat=now, exp=now + 86400 * 180, aud='https://appleid.apple.com', sub=self.client_id)\n", " return jwt.encode(payload, self.private_key, algorithm='ES256', headers={'kid': self.key_id})\n", @@ -272,6 +273,7 @@ " \n", " def get_info(self, token=None):\n", " \"Decode user info from the ID token\"\n", + " import jwt\n", " if token: self.token = token\n", " return jwt.decode(self.token.get('id_token'), options={\"verify_signature\": False})" ] diff --git a/settings.ini b/settings.ini index fc1bd274..b0c635db 100644 --- a/settings.ini +++ b/settings.ini @@ -4,8 +4,8 @@ lib_name = python-fasthtml version = 0.12.37 min_python = 3.10 license = apache2 -requirements = fastcore>=1.8.1 python-dateutil starlette>0.33 oauthlib itsdangerous uvicorn[standard]>=0.30 httpx fastlite>=0.1.1 python-multipart beautifulsoup4 PyJWT -dev_requirements = ipython lxml pysymbol_llm monsterui +requirements = fastcore>=1.8.1 python-dateutil starlette>0.33 oauthlib itsdangerous uvicorn[standard]>=0.30 httpx fastlite>=0.1.1 python-multipart beautifulsoup4 +dev_requirements = ipython lxml pysymbol_llm monsterui PyJWT black_formatting = False conda_user = fastai doc_path = _docs