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..2f50e22a 100644
--- a/fasthtml/oauth.py
+++ b/fasthtml/oauth.py
@@ -4,13 +4,13 @@
# %% 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 *
from oauthlib.oauth2 import WebApplicationClient
from urllib.parse import urlparse, urlencode, parse_qs, quote, unquote
-import secrets, httpx
+import secrets, httpx, time
# %% ../nbs/api/08_oauth.ipynb
class _AppClient(WebApplicationClient):
@@ -112,6 +112,33 @@ 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):
+ 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})
+
+ @client_secret.setter
+ 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})
+
# %% ../nbs/api/08_oauth.ipynb
@patch
def login_link(self:WebApplicationClient, redirect_uri, scope=None, state=None, **kwargs):
@@ -171,8 +198,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 +210,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..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"
+ "import secrets, httpx, time"
]
},
{
@@ -213,6 +213,71 @@
" 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",
+ " 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",
+ " \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",
+ " import jwt\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 +596,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 +608,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",
diff --git a/settings.ini b/settings.ini
index 9504a1b8..b0c635db 100644
--- a/settings.ini
+++ b/settings.ini
@@ -5,7 +5,7 @@ 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
-dev_requirements = ipython lxml pysymbol_llm monsterui
+dev_requirements = ipython lxml pysymbol_llm monsterui PyJWT
black_formatting = False
conda_user = fastai
doc_path = _docs