This example shows how to implement OAuth authentication for CLI apps using FastHTML. It allows users to authenticate with providers like Google and GitHub without manually copying tokens - similar to popular CLIs like GitHub and Railway.
server.py- The server component that handles OAuth redirects and token managementclient-login.py- The client component that initiates login and stores the authentication tokenclient-do.py- A client that uses the saved authentication to make requests
-
Start the server:
python server.py -
In another terminal, run the client to authenticate:
python client-login.py -
A browser window will open for authentication
-
After successful login, the session cookies are saved to
auth_token.txt -
You can now make authenticated API calls using the saved session:
python client-do.py
The authentication flow follows these steps:
-
Client Initialization:
- The client generates a random
paircodeas a unique identifier for this authentication session - This
paircodeconnects the CLI process to the browser authentication flow
# From client-login.py paircode = secrets.token_urlsafe(16)
- The client generates a random
-
Server Communication:
- The client sends the
paircodeto the server - The server stores this
paircodetemporarily and returns a login URL - The login URL includes the
paircodeas state parameter
# From client-login.py url = f'http://{host}/cli_login?paircode={paircode}' login_url = httpx.get(url).text
# From server.py @rt async def cli_login(request, paircode:str): pc_store[paircode] = None return cli.login_link(redir_url(request, redir_path), state=paircode)
- The client sends the
-
User Authentication:
- The client opens the login URL in the user's browser
- The user authenticates with the OAuth provider
- After approval, the provider redirects back with an authorization code (named
codein theserver.py)
# From client-login.py webbrowser.open(login_url)
-
Authentication Processing:
- The server receives the authorization code and the state (
paircode) - It exchanges the code for user authentication information
- The server associates this auth ID with the original
paircode - If the user is new, they are added to the database
# From server.py @rt(redir_path) async def redirect(request, code:str, state:str=None): redir = redir_url(request, redir_path) auth = cli.retr_id(code, redir) if state and state in pc_store: pc_store[state] = auth if auth not in users: users.insert(User(auth=auth)) return 'complete' else: return f"Failed to find {state} in {pc_store}"
- The server receives the authorization code and the state (
-
Session Creation and Retrieval:
- The client polls the server asking for authentication using the
paircode - Once available, the server creates a session with the auth ID and returns it
- The client saves the session cookies locally for future authenticated requests
# From server.py @rt async def token(paircode:str, sess): if pc_store.get(paircode, ''): auth = pc_store.pop(paircode) sess['auth'] = auth return auth
# From client-login.py def poll_token(paircode, host, interval=1, timeout=180): "Poll server for token until received or timeout" start = time() client = httpx.Client() while time()-start < timeout: resp = client.get(f"http://{host}/token?paircode={paircode}").raise_for_status() if resp.text.strip(): return dict(client.cookies) sleep(interval) # Save the session cookies cookies = poll_token(paircode, host) if cookies: with open(token_file, 'w') as f: json.dump(cookies, f) print(f"Token saved to {token_file}")
- The client polls the server asking for authentication using the
-
Using the Authentication:
- The client loads the saved session cookies
- It applies them to HTTP requests to authenticate with the server
- The server identifies the user based on the session cookie
# From client-do.py def get_client(cookie_file): client = httpx.Client() cookies = Path(cookie_file).read_json() client.cookies.update(cookies) return client client = get_client(token_file) url = f'http://{host}/secured' res = client.get(url).text print(res)
# From server.py - the secured endpoint @rt async def secured(sess): return sess['auth']
When the
/securedendpoint is called, it demonstrates authentication by retrieving the auth value from the session. This endpoint verifies and returns the user's identity (authentication) but doesn't implement authorization logic to control access. If the session doesn't contain an 'auth' value, the request would fail with a KeyError. To add authorization checks you could use FastHTML's OAuth class to manage access control.
For more detailed information about OAuth implementation in FastHTML, see the OAuth documentation.