mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-22 23:52:23 +02:00
Add pt 2 and 3
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
# Making Conversations Private
|
||||
|
||||
In this tutorial, we will extend our chatbot to give each user their own private conversations. We'll add [resource-level access control](../../concepts/auth.md#resource-level-access-control) so users can only see their own threads.
|
||||
|
||||
!!! note "This is part 2 of our authentication series:"
|
||||
|
||||
1. [Basic Authentication](getting_started.md) - Control who can access your bot
|
||||
2. Resource Authorization (you are here) - Let users have private conversations
|
||||
3. [Production Auth](add_auth_server.md) - Add real user accounts and validate using OAuth2
|
||||
|
||||
## Understanding Resource Authorization
|
||||
|
||||
In the last tutorial, we controlled who could access our bot. But right now, any authenticated user can see everyone else's conversations! Let's fix that by adding [resource authorization](../../concepts/auth.md#resource-authorization).
|
||||
|
||||
First, make sure you have completed the [Basic Authentication](getting_started.md) tutorial and that your secure bot can be run without errors:
|
||||
|
||||
```bash
|
||||
cd custom-auth
|
||||
pip install -e .
|
||||
langgraph dev --no-browser
|
||||
```
|
||||
|
||||
> - 🚀 API: http://127.0.0.1:2024
|
||||
> - 🎨 Studio UI: https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024
|
||||
> - 📚 API Docs: http://127.0.0.1:2024/docs
|
||||
|
||||
## Adding Resource Authorization
|
||||
|
||||
Recall that in the last tutorial, the [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object let us register an [authentication function](../../concepts/auth.md#authentication), which the LangGraph platform uses to validate the bearer tokens in incoming requests. Now we'll use it to register an **authorization** handler.
|
||||
|
||||
Authorization handlers are functions that run **after** authentication succeeds. These handlers can add [metadata](../../concepts/auth.md#resource-metadata) to resources (like who owns them) and filter what each user can see.
|
||||
|
||||
Let's update our `src/security/auth.py` and add one authorization handler that is run on every request:
|
||||
|
||||
```python hl_lines="29-39"
|
||||
from langgraph_sdk import Auth
|
||||
|
||||
# Keep our test users from the previous tutorial
|
||||
VALID_TOKENS = {
|
||||
"user1-token": {"id": "user1", "name": "Alice"},
|
||||
"user2-token": {"id": "user2", "name": "Bob"},
|
||||
}
|
||||
|
||||
auth = Auth()
|
||||
|
||||
|
||||
@auth.authenticate
|
||||
async def get_current_user(authorization: str | None) -> Auth.types.MinimalUserDict:
|
||||
"""Our authentication handler from the previous tutorial."""
|
||||
assert authorization
|
||||
scheme, token = authorization.split()
|
||||
assert scheme.lower() == "bearer"
|
||||
|
||||
if token not in VALID_TOKENS:
|
||||
raise Auth.exceptions.HTTPException(status_code=401, detail="Invalid token")
|
||||
|
||||
user_data = VALID_TOKENS[token]
|
||||
return {
|
||||
"identity": user_data["id"],
|
||||
}
|
||||
|
||||
|
||||
@auth.on
|
||||
async def add_owner(
|
||||
ctx: Auth.types.AuthContext, # Contains info about the current user
|
||||
value: dict, # The resource being created/accessed
|
||||
):
|
||||
"""Make resources private to their creator."""
|
||||
# Add owner when creating resources
|
||||
filters = {"owner": ctx.user.identity}
|
||||
metadata = value.setdefault("metadata", {})
|
||||
metadata.update(filters)
|
||||
|
||||
# Only let users see their own resources
|
||||
return filters
|
||||
```
|
||||
|
||||
The handler receives two parameters:
|
||||
|
||||
1. `ctx` ([AuthContext](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.AuthContext)): contains info about the current `user`, the user's `permissions`, the `resource` ("threads", "crons", "assistants"), and the `action` being taken ("create", "read", "update", "delete", "search", "create_run")
|
||||
2. `value` (`dict`): data that is being created or accessed. The contents of this dict depend on the resource and action being accessed. See [adding scoped authorization handlers](#scoped-authorization) below for information on how to get more tightly scoped access control.
|
||||
|
||||
Notice that our simple handler does two things:
|
||||
|
||||
1. Adds the user's ID to the resource's metadata.
|
||||
2. Returns a metadata filter so users only see resources they own.
|
||||
|
||||
## Testing Private Conversations
|
||||
|
||||
Let's test our authorization. Create a new file `test_private.py`:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from langgraph_sdk import get_client
|
||||
|
||||
async def test_private():
|
||||
# Create clients for both users
|
||||
alice = get_client(
|
||||
url="http://localhost:2024",
|
||||
headers={"Authorization": "Bearer user1-token"}
|
||||
)
|
||||
|
||||
bob = get_client(
|
||||
url="http://localhost:2024",
|
||||
headers={"Authorization": "Bearer user2-token"}
|
||||
)
|
||||
|
||||
# Alice creates a thread and chats
|
||||
alice_thread = await alice.threads.create()
|
||||
print(f"✅ Alice created thread: {alice_thread['thread_id']}")
|
||||
|
||||
await alice.runs.create(
|
||||
thread_id=alice_thread["thread_id"],
|
||||
assistant_id="agent",
|
||||
input={"messages": [{"role": "user", "content": "Hi, this is Alice's private chat"}]}
|
||||
)
|
||||
|
||||
# Bob tries to access Alice's thread
|
||||
try:
|
||||
await bob.threads.get(alice_thread["thread_id"])
|
||||
print("❌ Bob shouldn't see Alice's thread!")
|
||||
except Exception as e:
|
||||
print("✅ Bob correctly denied access:", e)
|
||||
|
||||
# Bob creates his own thread
|
||||
bob_thread = await bob.threads.create()
|
||||
await bob.runs.create(
|
||||
thread_id=bob_thread["thread_id"],
|
||||
assistant_id="agent",
|
||||
input={"messages": [{"role": "user", "content": "Hi, this is Bob's private chat"}]}
|
||||
)
|
||||
print(f"✅ Bob created his own thread: {bob_thread['thread_id']}")
|
||||
|
||||
# List threads - each user only sees their own
|
||||
alice_threads = await alice.threads.list()
|
||||
bob_threads = await bob.threads.list()
|
||||
print(f"✅ Alice sees {len(alice_threads)} thread")
|
||||
print(f"✅ Bob sees {len(bob_threads)} thread")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_private())
|
||||
```
|
||||
|
||||
Run the test code and you should see output like this:
|
||||
|
||||
```bash
|
||||
✅ Alice created thread: 533179b7-05bc-4d48-b47a-a83cbdb5781d
|
||||
✅ Bob correctly denied access: Client error '404 Not Found' for url 'http://localhost:2024/threads/533179b7-05bc-4d48-b47a-a83cbdb5781d'
|
||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
|
||||
✅ Bob created his own thread: 437c36ed-dd45-4a1e-b484-28ba6eca8819
|
||||
✅ Alice sees 1 thread
|
||||
✅ Bob sees 1 thread
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
1. Each user can create and chat in their own threads
|
||||
2. Users can't see each other's threads
|
||||
3. Listing threads only shows your own
|
||||
|
||||
## Adding scoped authorization handlers {#scoped-authorization}
|
||||
|
||||
The broad `@auth.on` handler matches on all [authorization events](../../concepts/auth.md#authorization-events). This is concise, but it means the contents of the `value` dict are not well-scoped, and we apply the same user-level access control to every resource. If we want to be more fine-grained, we can also control specific actions on resources.
|
||||
|
||||
Update `src/security/auth.py` to add handlers for specific resource types:
|
||||
|
||||
```python
|
||||
# Keep our previous handlers...
|
||||
|
||||
@auth.on.threads.create
|
||||
async def on_thread_create(
|
||||
ctx: Auth.types.AuthContext,
|
||||
value: Auth.types.on.threads.create.value,
|
||||
):
|
||||
"""Add owner when creating threads."""
|
||||
metadata = value.setdefault("metadata", {})
|
||||
metadata["owner"] = ctx.user.identity
|
||||
return {"owner": ctx.user.identity}
|
||||
|
||||
@auth.on.threads.read
|
||||
async def on_thread_read(
|
||||
ctx: Auth.types.AuthContext,
|
||||
value: Auth.types.on.threads.read.value,
|
||||
):
|
||||
"""Only let users read their own threads."""
|
||||
return {"owner": ctx.user.identity}
|
||||
|
||||
@auth.on.threads.create_run
|
||||
async def on_run_create(
|
||||
ctx: Auth.types.AuthContext,
|
||||
value: Auth.types.on.threads.create_run.value,
|
||||
):
|
||||
"""Only let thread owners create runs."""
|
||||
return {"owner": ctx.user.identity}
|
||||
|
||||
@auth.on.assistants
|
||||
async def on_assistants(
|
||||
ctx: Auth.types.AuthContext,
|
||||
value: Auth.types.on.assistants.value,
|
||||
):
|
||||
raise Auth.exceptions.HTTPException(
|
||||
status_code=403,
|
||||
detail="Not authorized to access assistants"
|
||||
)
|
||||
```
|
||||
|
||||
Notice that instead of one global handler, we now have specific handlers for:
|
||||
|
||||
1. Creating threads
|
||||
2. Reading threads
|
||||
3. Creating runs
|
||||
4. Accessing assistants
|
||||
|
||||
The first three of these match specific **actions** on each resource (see [resource actions](../../concepts/auth.md#resource-actions)), while the last one (`@auth.on.assistants`) matches _any_ action on the `assistants` resource. For each request, LangGraph will run the most specific handler that matches the resource and action being accessed. This means that the four handlers above will run rather than the broad "@auth.on" handler.
|
||||
|
||||
Try adding the following test code to `test_private.py`:
|
||||
|
||||
```python
|
||||
async def test_private():
|
||||
# ... Same as before
|
||||
# Try creating an assistant. This should fail
|
||||
try:
|
||||
await alice.assistants.create("agent")
|
||||
print("❌ Alice shouldn't be able to create assistants!")
|
||||
except Exception as e:
|
||||
print("✅ Alice correctly denied access:", e)
|
||||
|
||||
# Try searching for assistants. This also should fail
|
||||
try:
|
||||
await alice.assistants.search()
|
||||
print("❌ Alice shouldn't be able to search assistants!")
|
||||
except Exception as e:
|
||||
print("✅ Alice correctly denied access to searching assistants:", e)
|
||||
```
|
||||
|
||||
And then run the test code again:
|
||||
|
||||
```bash
|
||||
> python test_private.py
|
||||
✅ Alice created thread: dcea5cd8-eb70-4a01-a4b6-643b14e8f754
|
||||
✅ Bob correctly denied access: Client error '404 Not Found' for url 'http://localhost:2024/threads/dcea5cd8-eb70-4a01-a4b6-643b14e8f754'
|
||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
|
||||
✅ Bob created his own thread: 400f8d41-e946-429f-8f93-4fe395bc3eed
|
||||
✅ Alice sees 1 thread
|
||||
✅ Bob sees 1 thread
|
||||
✅ Alice correctly denied access:
|
||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500
|
||||
✅ Alice correctly denied access to searching assistants:
|
||||
```
|
||||
|
||||
Congratulations! You've built a chatbot where each user has their own private conversations. While this system uses simple token-based authentication, the authorization patterns we've learned will work with implementing any real authentication system. In the next tutorial, we'll replace our test users with real user accounts using OAuth2.
|
||||
|
||||
## What's Next?
|
||||
|
||||
Now that you can control access to resources, you might want to:
|
||||
|
||||
1. Move on to [Production Auth](add_auth_server.md) to add real user accounts
|
||||
2. Read more about [authorization patterns](../../concepts/auth.md#authorization)
|
||||
3. Try adding shared resources between users
|
||||
Reference in New Issue
Block a user