Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions examples/magickey_auth/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from fasthtml.common import *
from fasthtml.magickey import MagicKey
from plash_cli.auth import send_magiclink

db = database('data/data.db')
class User: id:int; email:str
class Passkey: id:str; user_id:int; public_key:bytes; sign_count:int
users = db.create(User)
passkeys = db.create(Passkey)

class Auth(MagicKey):
def get_user_id(self, email):
res = users(where="email = ?", where_args=[email])
if res: return res[0].id
return users.insert(User(email=email)).id
def has_passkey(self, email):
return bool(passkeys(where="user_id = ?", where_args=[self.get_user_id(email)]))
def get_passkey(self, cred_id):
try: r = passkeys[cred_id]
except NotFoundError: return None
return dict(email=users[r.user_id].email, public_key=r.public_key, sign_count=r.sign_count)
def save_passkey(self, cred_id, email, public_key, sign_count):
uid = self.get_user_id(email)
passkeys.insert(Passkey(id=cred_id, user_id=uid, public_key=public_key, sign_count=sign_count))
def update_passkey(self, cred_id, sign_count):
passkeys.update(Passkey(id=cred_id, sign_count=sign_count))

def send_email(email, url):
res = send_magiclink(email,url)
if res.status_code == 200: return P(f'Your magic login link has beent sent to: {email}.')
else: return P('Something went wrong, try again later.')

app, rt = fast_app()
mk = Auth(app, send_email=send_email)

@rt('/')
def home(auth):
u = users[auth]
return P(f'Hello {u.email}!'), A('Log out', href='/logout')

@rt('/login')
def login(error: str=None):
errmsg = P(error.replace('_', ' ').title(), style='color:red') if error else ''
return Titled('Sign In', errmsg,
Button('Sign in with Passkey', hx_post='/request_passkey_auth', target_id='scripts'),
Hr(),
Form(method='post', action='/send_magic_link')(
Input(name='email', type='email', placeholder='you@example.com'),
Button('Send Magic Link')),
Div(id='scripts'))

@rt('/setup_passkey')
def setup_passkey(): return Titled('Set Up Passkey',
P('Set up a passkey for faster logins next time?'),
Button('Register Passkey', hx_post='/request_passkey_reg', target_id='scripts'),
Form(Button('Skip'), method='post', action='/skip_passkey_reg'),
Div(id='scripts'))

serve()
4 changes: 4 additions & 0 deletions examples/magickey_auth/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
plash_cli @ git+https://github.com/AnswerDotAI/plash_cli@magickey
python-fasthtml @ git+https://github.com/AnswerDotAI/fasthtml@magickey
webauthn
numpy
59 changes: 49 additions & 10 deletions nbs/01_auth.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@
"> Client side logic to add Plash Auth to your app"
]
},
{
"cell_type": "markdown",
"id": "4416a9b8",
"metadata": {},
"source": [
"## Google Auth"
]
},
{
"cell_type": "markdown",
"id": "188fb79d",
"metadata": {},
"source": [
"This page describes how Plash Auth is implemented client side. \n",
"This section describes how Plash Auth implements Google Auth client side. \n",
"\n",
"Please see the [how to](how_to/auth.html) for instructions on how to use it."
]
Expand All @@ -25,7 +33,7 @@
"id": "59ce8def",
"metadata": {},
"source": [
"## Setup -"
"### Setup -"
]
},
{
Expand Down Expand Up @@ -73,7 +81,7 @@
"source": [
"#| export\n",
"def _signin_url(email_re: str=None, hd_re: str=None):\n",
" res = httpx.post(os.environ['PLASH_AUTH_URL'], json=dict(email_re=email_re, hd_re=hd_re), \n",
" res = httpx.post(os.environ['PLASH_AUTH_URL']+'/request_signin', json=dict(email_re=email_re, hd_re=hd_re), \n",
" auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']), \n",
" headers={'X-PLASH-AUTH-VERSION': __version__}).raise_for_status().json()\n",
" if \"warning\" in res: warn(res.pop('warning'))\n",
Expand Down Expand Up @@ -116,8 +124,8 @@
"source": [
"#| export\n",
"def mk_signin_url(session: dict, # Session dictionary\n",
" email_re: str=None, # Regex filter for allowed email addresses\n",
" hd_re: str=None): # Regex filter for allowed Google hosted domains\n",
" email_re: str=None, # Regex filter for allowed email addresses\n",
" hd_re: str=None): # Regex filter for allowed Google hosted domains\n",
" \"Generate a Google Sign-In URL for Plash authentication.\"\n",
" if not _in_prod: return f\"{signin_completed_rt}?signin_reply=mock-sign-in-reply\"\n",
" res = _signin_url(email_re, hd_re)\n",
Expand Down Expand Up @@ -216,6 +224,40 @@
"When testing locally this will always return the mock Google ID `'424242424242424242424'`."
]
},
{
"cell_type": "markdown",
"id": "ba560a3a",
"metadata": {},
"source": [
"## Magic Link"
]
},
{
"cell_type": "markdown",
"id": "1c7b6cd6",
"metadata": {},
"source": [
"Pla.sh provides a service where you can send magic links for sign-up or login to your users for free. "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ad5693c9",
"metadata": {},
"outputs": [],
"source": [
"#| export\n",
"def send_magiclink(email: str, # Email address to send magic link to\n",
" url: str): # Magic link URL (must match app's domain)\n",
" \"Send a magic link email to the given address via Plash Auth.\"\n",
" return httpx.post(os.environ['PLASH_AUTH_URL']+'/send_magiclink',\n",
" json=dict(email=email, url=url),\n",
" auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']),\n",
" headers={'X-PLASH-AUTH-VERSION': __version__}\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "72eaabaa",
Expand All @@ -238,11 +280,8 @@
}
],
"metadata": {
"kernelspec": {
"display_name": "python3",
"language": "python",
"name": "python3"
}
"solveit_dialog_mode": "learning",
"solveit_ver": 2
},
"nbformat": 4,
"nbformat_minor": 5
Expand Down
3 changes: 2 additions & 1 deletion plash_cli/_modidx.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
'plash_cli.auth._parse_jwt': ('auth.html#_parse_jwt', 'plash_cli/auth.py'),
'plash_cli.auth._signin_url': ('auth.html#_signin_url', 'plash_cli/auth.py'),
'plash_cli.auth.goog_id_from_signin_reply': ('auth.html#goog_id_from_signin_reply', 'plash_cli/auth.py'),
'plash_cli.auth.mk_signin_url': ('auth.html#mk_signin_url', 'plash_cli/auth.py')},
'plash_cli.auth.mk_signin_url': ('auth.html#mk_signin_url', 'plash_cli/auth.py'),
'plash_cli.auth.send_magiclink': ('auth.html#send_magiclink', 'plash_cli/auth.py')},
'plash_cli.cli': { 'plash_cli.cli.Path._is_dir_empty': ('cli.html#path._is_dir_empty', 'plash_cli/cli.py'),
'plash_cli.cli.PlashError': ('cli.html#plasherror', 'plash_cli/cli.py'),
'plash_cli.cli._app_list': ('cli.html#_app_list', 'plash_cli/cli.py'),
Expand Down
18 changes: 14 additions & 4 deletions plash_cli/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_auth.ipynb.

# %% auto #0
__all__ = ['signin_completed_rt', 'mk_signin_url', 'PlashAuthError', 'goog_id_from_signin_reply']
__all__ = ['signin_completed_rt', 'mk_signin_url', 'PlashAuthError', 'goog_id_from_signin_reply', 'send_magiclink']

# %% ../nbs/01_auth.ipynb #c6aa552a
import httpx,os,jwt
Expand All @@ -17,7 +17,7 @@

# %% ../nbs/01_auth.ipynb #e6590075
def _signin_url(email_re: str=None, hd_re: str=None):
res = httpx.post(os.environ['PLASH_AUTH_URL'], json=dict(email_re=email_re, hd_re=hd_re),
res = httpx.post(os.environ['PLASH_AUTH_URL']+'/request_signin', json=dict(email_re=email_re, hd_re=hd_re),
auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']),
headers={'X-PLASH-AUTH-VERSION': __version__}).raise_for_status().json()
if "warning" in res: warn(res.pop('warning'))
Expand All @@ -28,8 +28,8 @@ def _signin_url(email_re: str=None, hd_re: str=None):

# %% ../nbs/01_auth.ipynb #74a0a24d
def mk_signin_url(session: dict, # Session dictionary
email_re: str=None, # Regex filter for allowed email addresses
hd_re: str=None): # Regex filter for allowed Google hosted domains
email_re: str=None, # Regex filter for allowed email addresses
hd_re: str=None): # Regex filter for allowed Google hosted domains
"Generate a Google Sign-In URL for Plash authentication."
if not _in_prod: return f"{signin_completed_rt}?signin_reply=mock-sign-in-reply"
res = _signin_url(email_re, hd_re)
Expand Down Expand Up @@ -58,3 +58,13 @@ def goog_id_from_signin_reply(session: dict, # Session dictionary containing 're
if session.get('req_id') != parsed['req_id']: raise PlashAuthError("Request originated from a different browser than the one receiving the reply")
if parsed['err']: raise PlashAuthError(f"Authentication failed: {parsed['err']}")
return parsed['sub']

# %% ../nbs/01_auth.ipynb #ad5693c9
def send_magiclink(email: str, # Email address to send magic link to
url: str): # Magic link URL (must match app's domain)
"Send a magic link email to the given address via Plash Auth."
return httpx.post(os.environ['PLASH_AUTH_URL']+'/send_magiclink',
json=dict(email=email, url=url),
auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']),
headers={'X-PLASH-AUTH-VERSION': __version__}
)
Loading