Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1919ee2
add public pem
RensDimmendaal Jun 18, 2025
95f86e4
wip oauth
RensDimmendaal Jun 18, 2025
1b42f67
Merge branch 'main' into feat-add-oauth
RensDimmendaal Jul 2, 2025
e74b1e9
add mock lib and trial public key
RensDimmendaal Jul 8, 2025
53b420f
clean up
RensDimmendaal Jul 8, 2025
d7ef1ca
add missing dep
RensDimmendaal Jul 8, 2025
defbbd6
rm unused vars
RensDimmendaal Jul 8, 2025
1b8af8e
test depr version
RensDimmendaal Jul 9, 2025
fe332c1
Replace print with warnings.warn for version warnings
RensDimmendaal Jul 9, 2025
1a14b2e
Restore version to 0.2.1
RensDimmendaal Jul 9, 2025
12fab11
raise errors
RensDimmendaal Jul 9, 2025
f362a03
add howto
RensDimmendaal Jul 10, 2025
1352e04
add todos to auth howto
RensDimmendaal Jul 10, 2025
80a440a
Fix logout to only clear user_id instead of entire session
RensDimmendaal Jul 11, 2025
2c5c77a
updat auth endpoint and docs
RensDimmendaal Jul 18, 2025
ca4ed23
simplify auth.py
RensDimmendaal Jul 18, 2025
e8381ce
simplify local use of plash auth
RensDimmendaal Jul 22, 2025
4d127f4
update example for simple local auth
RensDimmendaal Jul 22, 2025
831d774
inspect data
RensDimmendaal Jul 22, 2025
625ef86
simplify auth.py
RensDimmendaal Jul 22, 2025
2efd589
wip - plash_auth_url
RensDimmendaal Jul 22, 2025
a978e12
shorter regex var names
RensDimmendaal Jul 22, 2025
2f66f74
wip - add debug prints
RensDimmendaal Jul 23, 2025
b30e1ce
fixes email_re error catch
RensDimmendaal Jul 23, 2025
d104f42
rename auth key to req_id consistently
RensDimmendaal Jul 23, 2025
ac51e10
dedup app_id in signin url request
RensDimmendaal Jul 23, 2025
5f99d2a
mv auth module to nb
RensDimmendaal Jul 25, 2025
b18629f
improve auth howto docs
RensDimmendaal Jul 31, 2025
919dc38
add explainer for restricting auth access
RensDimmendaal Aug 4, 2025
cb1b134
remove valid key as it did not check anything
RensDimmendaal Aug 4, 2025
5cae214
use get for seession id as it may not exist
RensDimmendaal Aug 4, 2025
a6c2523
Merge branch 'main' into feat-add-oauth
RensDimmendaal Aug 4, 2025
b7eb5c1
change deps for merge to main
RensDimmendaal Aug 4, 2025
fde28fd
bump
RensDimmendaal Aug 4, 2025
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
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ include LICENSE
include CONTRIBUTING.md
include README.md
recursive-exclude * __pycache__
include plash_cli/assets/es256_public_key.pem
34 changes: 34 additions & 0 deletions examples/auth/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from fasthtml.common import *
from plash_cli.auth import *

app, rt = fast_app()

@rt
def index(session):
if uid:=session.get('uid'): # <1>
return (H1(f"Welcome! You are logged in as user: {uid}"),
A("Logout", href="/logout"))
else:
return (
H1("Welcome! Please sign."),
A("Sign in with Google", href=mk_signin_url(session))) # <2>

@rt(signin_completed_rt) # <3>
def signin_completed(session, signin_reply: str):
try:
uid = goog_id_from_signin_reply(session, signin_reply) # <4>
session['uid'] = uid
return RedirectResponse('/', status_code=303)
except PlashAuthError as e: # <5>
return Div(
H2("Login Failed"),
P(f"There was an error signing you in: {e}"),
A("Try Again", href="/")
)

@rt('/logout')
def logout(session):
session.pop('uid') # <6>
return RedirectResponse('/', status_code=303)

serve()
2 changes: 2 additions & 0 deletions examples/auth/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
python-fasthtml
plash-cli
1 change: 1 addition & 0 deletions examples/with_db/data/file1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hey there!
240 changes: 240 additions & 0 deletions nbs/01_auth.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "e41f2262",
"metadata": {},
"source": [
"# Auth\n",
"\n",
"> Client side logic to add Plash Auth to your app"
]
},
{
"cell_type": "markdown",
"id": "188fb79d",
"metadata": {},
"source": [
"For an end2end example on how to use this module see `./examples/auth/main.py`"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d1d44038",
"metadata": {},
"outputs": [],
"source": [
"#| default_exp auth"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c6aa552a",
"metadata": {},
"outputs": [],
"source": [
"#| export\n",
"import httpx,os,jwt\n",
"from pathlib import Path\n",
"from warnings import warn\n",
"\n",
"from plash_cli import __version__"
]
},
{
"cell_type": "markdown",
"id": "980c7492",
"metadata": {},
"source": [
"The signin completion route is where Google redirects users after authentication. Your app needs to add this route to handle the OAuth callback."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4b15411c",
"metadata": {},
"outputs": [],
"source": [
"#| export\n",
"signin_completed_rt = \"/signin_completed\""
]
},
{
"cell_type": "markdown",
"id": "a4873761",
"metadata": {},
"source": [
"The production flag lets developers use mock authentication during development."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "718ef4d6",
"metadata": {},
"outputs": [],
"source": [
"#| export\n",
"_in_prod = os.getenv('PLASH_PRODUCTION', '') == '1'"
]
},
{
"cell_type": "markdown",
"id": "f15e27d0",
"metadata": {},
"source": [
"This function makes the actual HTTP request to the Plash authentication service. It sends the email and domain filters to get back a signin URL and request ID that we'll use to track this authentication attempt."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "73a7d63b",
"metadata": {},
"outputs": [],
"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",
" 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",
" return res"
]
},
{
"cell_type": "markdown",
"id": "acf6707c",
"metadata": {},
"source": [
"This is the main function your app calls to get a Google signin URL. In development mode, it returns a mock URL to make testing easier. In production, it calls the Plash auth service and stores the request ID in the session for later verification."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "74a0a24d",
"metadata": {},
"outputs": [],
"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",
" \"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",
" session['req_id'] = res['req_id']\n",
" return res['plash_signin_url']"
]
},
{
"cell_type": "markdown",
"id": "1b33d30d",
"metadata": {},
"source": [
"After Google authentication, Plash sends back a JSON Web Token (JWT) containing the user's information. This function decodes and validates that token using the ES256 public key. If anything goes wrong with the JWT, it returns error details instead of crashing.\n",
"\n",
"Note: a JWT does not mean the message is encrypted. It ensures data integrity and authenticity, it protects against tampering and forgery. We use JWT tokens so your app can trust that the sign-in information and user details it receives after authentication really come from Plash (and by extension, Google), and have not been modified by an attacker."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8e66bb6e",
"metadata": {},
"outputs": [],
"source": [
"#| export\n",
"def _parse_jwt(reply: str) -> dict:\n",
" \"Parse JWT reply and return decoded claims or error info\"\n",
" try: decoded = jwt.decode(reply, key=open(Path(__file__).parent / \"assets\" / \"es256_public_key.pem\",\"rb\").read(), algorithms=[\"ES256\"], \n",
" options=dict(verify_aud=False, verify_iss=False))\n",
" except Exception as e: return dict(req_id=None, sub=None, err=f'JWT validation failed: {e}')\n",
" return dict(req_id=decoded.get('req_id'), sub=decoded.get('sub'), err=decoded.get('err'))"
]
},
{
"cell_type": "markdown",
"id": "6c1fe53f",
"metadata": {},
"source": [
"A custom exception for when authentication fails. This makes it easier for your app to handle auth errors specifically.\n",
"See `./examples/auth/main.py` for an example on how you can catch this exception in your application."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0e61fbdd",
"metadata": {},
"outputs": [],
"source": [
"#| export\n",
"class PlashAuthError(Exception):\n",
" \"\"\"Raised when Plash authentication fails\"\"\"\n",
" pass"
]
},
{
"cell_type": "markdown",
"id": "62320729",
"metadata": {},
"source": [
"This is the main function your app calls in the signin completion route. It verifies the JWT reply matches the original request (preventing CSRF attacks), checks for any authentication errors, and returns the user's Google ID if everything is valid.\n",
"\n",
"When testing locally this will always return the mock Google ID `'424242424242424242424'`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a311ce9a",
"metadata": {},
"outputs": [],
"source": [
"#| export\n",
"def goog_id_from_signin_reply(session: dict, # Session dictionary containing 'req_id'\n",
" reply: str): # The JWT reply string from Plash after Google authentication\n",
" \"Validate Google sign-in reply and returns Google user ID if valid.\"\n",
" if not _in_prod: return '424242424242424242424'\n",
" parsed = _parse_jwt(reply)\n",
" if session.get('req_id') != parsed['req_id']: raise PlashAuthError(\"Request originated from a different browser than the one receiving the reply\")\n",
" if parsed['err']: raise PlashAuthError(f\"Authentication failed: {parsed['err']}\")\n",
" return parsed['sub']"
]
},
{
"cell_type": "markdown",
"id": "72eaabaa",
"metadata": {},
"source": [
"## Export -"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "640ce52d",
"metadata": {},
"outputs": [],
"source": [
"#|hide\n",
"from nbdev import nbdev_export\n",
"nbdev_export()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "python3",
"language": "python",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading