Skip to content

Commit

Permalink
[Docs] Add auth tutorial
Browse files Browse the repository at this point in the history
  • Loading branch information
hinthornw committed Dec 17, 2024
1 parent d9b7aaa commit 245cf83
Show file tree
Hide file tree
Showing 4 changed files with 344 additions and 2 deletions.
6 changes: 6 additions & 0 deletions docs/docs/how-tos/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r
- [How to deploy to a self-hosted environment](./deploy-self-hosted.md)
- [How to interact with the deployment using RemoteGraph](./use-remote-graph.md)

<!-- ### Authentication & Access Control
- [How to implement custom authentication handlers](./auth/custom_handlers.md)
- [How to configure endpoint-specific authorization rules](./auth/endpoint_rules.md)
- [How to integrate with identity providers](./auth/identity_providers.md) -->

### Assistants

[Assistants](../concepts/assistants.md) is a configured instance of a template.
Expand Down
326 changes: 326 additions & 0 deletions docs/docs/tutorials/auth/getting_started.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
# Setting up custom authentication

Let's learn how to add custom authentication to a LangGraph Platformdeployment. We'll cover the core concepts of token-based authentication and show how to integrate with an authentication server.

??? note "Default authentication"
When deploying to LangGraph Cloud, requests are authenticated using LangSmith API keys by default. This gates access to the server but doesn't provide fine-grained access control over threads. Self-hosted LangGraph platform has no default authentication. This guide shows how to add custom authentication handlers that work in both cases, to provide fine-grained access control over threads, runs, and other resources.

## Understanding authentication flow

The key components in a token-based authentication system are:

1. **Auth server**: manages users and generates signed tokens (could be Supabase, Auth0, or your own server)
2. **Client**: gets tokens from auth server and includes them in requests. This is typically the user's browser or mobile app.
3. **LangGraph backend**: validates tokens and enforces access control to control access to your agents and data.

After implementing the following steps, when a user's client application (such as their web browser or mobile app) wants to access resources in LangGraph, the following steps occur:

1. User authenticates with the auth server (username/password, OAuth, "Sign in with Google", etc.)
2. Auth server returns a signed JWT token attesting "I am user X with claims/roles Y"
3. User includes this token in request headers to LangGraph
4. LangGraph validates token signature and checks claims against the auth server. If valid, it allows the request, using custom filters to restrict access only to the user's resources.

In this tutorial, we'll implement password-based authentication using Supabase as our auth server.

## Setting up the project

First, clone the example template:

```bash
git clone https://github.com/langchain-ai/custom-auth.git
cd custom-auth
```

This contains our chatbot code, as well as a custom auth handler (discussed below).

### Configure Supabase

1. Create a new project at [supabase.com](https://supabase.com)
2. Go to Project Settings > API to find your project's credentials
3. Add these credentials to your `.env` file:

```bash
cp .env.example .env
```

Add the following to your `.env`:

```bash
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_KEY=your-service-key # aka the service_role secret
SUPABASE_JWT_SECRET=your-jwt-secret
ANTHROPIC_API_KEY=your-anthropic-key # For the LLM in our chatbot
```

Additionally, note down your project's "anon public" key. This public key will be used by the user's client to authenticate with Supabase.

### Start the server

Install dependencies and start the LangGraph server:

```bash
pip install -U "langgraph-cli[inmem]" && pip install -e .
langgraph dev --no-browser
```

## Interacting with the server

First, let's set up our environment and helper functions. Fill in the values for your Supabase anon key, and provide a working email address for our test users. You can use a single email with "+" to create multiple users, e.g. "[email protected]" and "[email protected]".

```python
import os
import httpx
import dotenv

from langgraph_sdk import get_client

dotenv.load_dotenv()

supabase_url: str = os.environ.get("SUPABASE_URL")
supabase_anon_key: str = "CHANGEME" # Your project's anon/public key
user_1_email = "CHANGEME" # Your test email
user_2_email = "CHANGEME" # A second test email
password = "password" # Very secure! :)

# Helper functions for authentication
async def sign_up(email, password):
async with httpx.AsyncClient() as client:
response = await client.post(
f"{supabase_url}/auth/v1/signup",
headers={
"apikey": supabase_anon_key,
"Content-Type": "application/json",
},
json={
"email": email,
"password": password
}
)
if response.status_code == 200:
return response.json()
else:
raise ValueError("Sign up failed:", response.status_code, response.text)

async def login(email, password):
async with httpx.AsyncClient() as client:
response = await client.post(
f"{supabase_url}/auth/v1/token?grant_type=password",
headers={
"apikey": supabase_anon_key,
"Content-Type": "application/json",
},
json={
"email": email,
"password": password
}
)
if response.status_code == 200:
return response.json()
else:
raise ValueError("Login failed:", response.status_code, response.text)
```

Now let's create two test users:

```python
# Create our test users
await sign_up(user_1_email, password)
await sign_up(user_2_email, password)
```

⚠️ Before continuing: Check your email for both addresses and click the confirmation links. Don't worry about any error pages you might see from the confirmation redirect - those would normally be handled by your frontend.

Now let's log in as our first user and create a thread:

```python
# Log in as user 1
user_1_login_data = await login(user_1_email, password)
user_1_token = user_1_login_data["access_token"]

# Create an authenticated client
client = get_client(
url="http://localhost:2024",
headers={"Authorization": f"Bearer {user_1_token}"}
)

# Create a thread and chat with the bot
thread = await client.threads.create()
print(f'Created thread: {thread["thread_id"]}')

# Have a conversation
async for event, (chunk, metadata) in client.runs.stream(
thread_id=thread["thread_id"],
assistant_id="agent",
input={"messages": [{"role": "user", "content": "Tell me a short joke"}]},
stream_mode="messages-tuple",
):
if event == "messages" and metadata["langgraph_node"] == "chatbot":
print(chunk['content'], end="", flush=True)

# View the thread history
thread = await client.threads.get(thread["thread_id"])
print(f"\nThread:\n{thread}")
```

Now let's see what happens when we try to access without authentication:

```python
# Try to access without a token
unauthenticated_client = get_client(url="http://localhost:2024")
try:
await unauthenticated_client.threads.create()
except Exception as e:
print(f"Failed without token: {e}") # Will show 403 Forbidden
```

Finally, let's try accessing user 1's thread as user 2:

```python
# Log in as user 2
user_2_login_data = await login(user_2_email, password)
user_2_token = user_2_login_data["access_token"]

# Create client for user 2
user_2_client = get_client(
url="http://localhost:2024",
headers={"Authorization": f"Bearer {user_2_token}"}
)

# Try to access user 1's thread
try:
await user_2_client.threads.get(thread["thread_id"])
except Exception as e:
print(f"Failed to access other user's thread: {e}") # Will show 404 Not Found
```

This demonstrates that:

1. With a valid token, we can create and interact with threads
2. Without a token, we get a 401 Unauthorized or 403 Forbidden error
3. Even with a valid token, users can only access their own threads

Now let's look at how this works under the hood.

## How it works: The authentication handler

All of this is enabled by our custom authentication handler, which is registered in `auth.py`, configured in our `langgraph.json` file:

```json
{
...
"auth": {
"path": "src/security/auth.py:auth"
}
}
```

This tells the LangGraph platform to look for your variable names `auth` (of type `Auth`) in the file located at `src/security/auth.py`. If you open `src/security/auth.py` now, you'll see code that looks similar to the following:

```python
# src/security/auth.py
import os
import httpx
import jwt
from langgraph_sdk import Auth

# These are configured in your .env file
SUPABASE_URL = os.environ["SUPABASE_URL"]
SUPABASE_SERVICE_KEY = os.environ["SUPABASE_SERVICE_KEY"]
SUPABASE_JWT_SECRET = os.environ["SUPABASE_JWT_SECRET"]

auth = Auth()

@auth.authenticate
async def get_current_user(
authorization: str | None, # "Bearer <token>"
) -> tuple[list[str], Auth.types.MinimalUserDict]:
if not authorization:
raise Auth.exceptions.HTTPException(
status_code=401,
detail="Not authenticated",
headers={"WWW-Authenticate": "Bearer"},
)

try:
# Extract and validate JWT token
token = authorization.split(" ", 1)[1]
payload = jwt.decode(
token,
SUPABASE_JWT_SECRET,
algorithms=["HS256"],
audience="authenticated",
)

# Verify with Supabase
async with httpx.AsyncClient() as client:
response = await client.get(
f"{SUPABASE_URL}/auth/v1/user",
headers={"Authorization": f"Bearer {token}"},
)
if response.status_code != 200:
raise Auth.exceptions.HTTPException(
status_code=401,
detail="Invalid token"
)

user_data = response.json()
return [], {
"identity": user_data["id"],
"display_name": user_data.get("name"),
"is_authenticated": True,
}
except Exception as e:
raise Auth.exceptions.HTTPException(
status_code=401,
detail="Invalid token"
)
```

This handler:

1. Gets the token from the Authorization header
2. Verifies it was signed by Supabase
3. Double-checks with Supabase that the token is still valid
4. Returns the user's information for use in our app

## Managing user resources

We can also ensure users can only access their own resources:

```python
@auth.on
async def add_owner(
ctx: Auth.types.AuthContext,
value: dict,
):
"""Add owner to resource metadata and filter by owner."""
filters = {"owner": ctx.user.identity}
metadata = value.setdefault("metadata", {})
metadata.update(filters)
return filters
```

This handler matches ALL requests to threads, runs, assistants, crons, and other resources. It does 2 things:

1. Adds the user's ID as owner when creating resources
2. Filters resources by owner when reading them


## Deploying to LangGraph Cloud

Now that you've set everything up, you can deploy your LangGraph application to LangGraph Cloud! Simply:
1. Push your code to a new github repository.
2. Navigate to the LangGraph Platform page and click "+New Deployment".
3. Connect to your GitHub repository and copy the contents of your `.env` file as environment variables.
4. Click "Submit".

Once deployed, you should be able to run the code above, replacing the `http://localhost:2024` with the URL of your deployment.


## Next steps

Now that you understand token-based authentication:

1. Add password hashing and secure user management
2. Add user-specific resource ownership (see [resource access control](./resource_access.md))
3. Implement more advanced auth patterns
12 changes: 10 additions & 2 deletions docs/docs/tutorials/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ title: Tutorials

New to LangGraph or LLM app development? Read this material to get up and running building your first applications.

## Get Started 🚀 {#quick-start}
## Get Started

- [LangGraph Quickstart](introduction.ipynb): Build a chatbot that can use tools and keep track of conversation history. Add human-in-the-loop capabilities and explore how time-travel works.
- [LangGraph Server Quickstart](langgraph-platform/local-server.md): Launch a LangGraph server locally and interact with it using the REST API and LangGraph Studio Web UI.
- [LangGraph Cloud QuickStart](../cloud/quick_start.md): Deploy a LangGraph app using LangGraph Cloud.
- [LangGraph Template Quickstart](../concepts/template_applications.md): Quickly start building with LangGraph Platform using a template application.

## Use cases 🛠️
## Use cases

Explore practical implementations tailored for specific scenarios:

Expand Down Expand Up @@ -71,3 +71,11 @@ Explore practical implementations tailored for specific scenarios:
- [Web Navigation](web-navigation/web_voyager.ipynb): Build an agent that can navigate and interact with websites
- [Competitive Programming](usaco/usaco.ipynb): Build an agent with few-shot "episodic memory" and human-in-the-loop collaboration to solve problems from the USA Computing Olympiad; adapted from the ["Can Language Models Solve Olympiad Programming?"](https://arxiv.org/abs/2404.10952v1) paper by Shi, Tang, Narasimhan, and Yao.
- [Complex data extraction](extraction/retries.ipynb): Build an agent that can use function calling to do complex extraction tasks

## LangGraph Platform

### Authentication & Access Control

Learn how to secure your LangGraph applications:

- [Setting Up Custom Authentication](./auth/getting_started.md): Implement OAuth2 authentication to authorize users on your deployment
2 changes: 2 additions & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ nav:
- tutorials/web-navigation/web_voyager.ipynb
- tutorials/usaco/usaco.ipynb
- tutorials/extraction/retries.ipynb
- LangGarph Platform:
- docs/docs/tutorials/auth/getting_started.md

- How-to Guides:
- how-tos/index.md
Expand Down

0 comments on commit 245cf83

Please sign in to comment.