From 245cf83b20f7fc4d5f03928e13b0f5583d146968 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 17 Dec 2024 08:29:51 -0800 Subject: [PATCH 01/29] [Docs] Add auth tutorial --- docs/docs/how-tos/index.md | 6 + docs/docs/tutorials/auth/getting_started.md | 326 ++++++++++++++++++++ docs/docs/tutorials/index.md | 12 +- docs/mkdocs.yml | 2 + 4 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 docs/docs/tutorials/auth/getting_started.md diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index f33d97340..193564a67 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -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) + + ### Assistants [Assistants](../concepts/assistants.md) is a configured instance of a template. diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md new file mode 100644 index 000000000..1871a9b08 --- /dev/null +++ b/docs/docs/tutorials/auth/getting_started.md @@ -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. "myemail+1@gmail.com" and "myemail+2@gmail.com". + +```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 " +) -> 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 diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md index 286137479..65cafb115 100644 --- a/docs/docs/tutorials/index.md +++ b/docs/docs/tutorials/index.md @@ -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: @@ -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 \ No newline at end of file diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 30adba64e..1fb71d807 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -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 From 3dd1e679772f513bb5c4e36d4b087ea5ddc85977 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 17 Dec 2024 09:49:18 -0800 Subject: [PATCH 02/29] Add mermaid --- docs/docs/tutorials/auth/getting_started.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md index 1871a9b08..7546a5f74 100644 --- a/docs/docs/tutorials/auth/getting_started.md +++ b/docs/docs/tutorials/auth/getting_started.md @@ -1,6 +1,6 @@ # 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. +Let's learn how to add custom authentication to a LangGraph Platform deployment. 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. @@ -15,6 +15,21 @@ The key components in a token-based authentication system are: 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: +```mermaid +sequenceDiagram + participant User + participant AuthServer as Auth Server + participant LangGraph + + User->>AuthServer: 1. Authenticate (username/password) + AuthServer-->>User: 2. Return signed JWT token + User->>LangGraph: 3. Request with JWT in header + LangGraph->>AuthServer: 4a. Validate token + AuthServer-->>LangGraph: 4b. Confirm token validity + Note over LangGraph: 4c. Apply access filters + LangGraph-->>User: Return authorized resources +``` + 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 @@ -309,6 +324,7 @@ This handler matches ALL requests to threads, runs, assistants, crons, and other ## 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. From 23e18e8c1b07df6c88fb6493649152696e273f99 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:50:09 -0800 Subject: [PATCH 03/29] Add how-tos --- docs/docs/how-tos/auth/custom_auth.md | 122 +++++++++++++++++++++ docs/docs/how-tos/auth/openapi_security.md | 95 ++++++++++++++++ docs/docs/how-tos/index.md | 7 +- 3 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 docs/docs/how-tos/auth/custom_auth.md create mode 100644 docs/docs/how-tos/auth/openapi_security.md diff --git a/docs/docs/how-tos/auth/custom_auth.md b/docs/docs/how-tos/auth/custom_auth.md new file mode 100644 index 000000000..872c3c216 --- /dev/null +++ b/docs/docs/how-tos/auth/custom_auth.md @@ -0,0 +1,122 @@ +# How to add custom authentication + +This guide shows how to add custom authentication to your LangGraph Platform application. This guide applies to both LangGraph Cloud, BYOC, and self-hosted deployments. It does not apply to isolated usage of the LangGraph open source library in your own custom server. + + + +## 1. Implement authentication + +Create `auth.py` file, with a basic JWT authentication handler: + +```python +from langgraph_sdk import Auth + +my_auth = Auth() + +@my_auth.authenticate +async def authenticate(authorization: str) -> str: + token = authorization.split(" ", 1)[-1] # "Bearer " + try: + # Verify token with your auth provider + user_id = await verify_token(token) + return user_id + except Exception: + raise Auth.exceptions.HTTPException( + status_code=401, + detail="Invalid token" + ) + +# Optional: Add authorization rules +@my_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 +``` + +## 2. Update configuration + +In your `langgraph.json`, add the path to your auth file: + +```json hl_lines="7-9" +{ + "dependencies": ["."], + "graphs": { + "agent": "./agent.py:graph" + }, + "env": ".env", + "auth": { + "path": "./auth.py:my_auth" + } +} +``` + +## Connect from the client + +Once you've set up authentication in your server, requests must include the the required authorization information based on your chosen scheme. +Assuming you are using JWT token authentication, you could access your deployments using any of the following methods: + +=== "Python Client" + +```python +from langgraph_sdk import get_client +# generate token with your auth provider +my_token = "your-token" +client = get_client( + url="http://localhost:2024", + headers={"Authorization": f"Bearer {my_token}"} +) +threads = await client.threads.list() +``` + +=== "Python RemoteGraph" + +```python +from langgraph.pregel.remote import RemoteGraph +# generate token with your auth provider +my_token = "your-token" +remote_graph = RemoteGraph( + "agent", + url="http://localhost:2024", + headers={"Authorization": f"Bearer {my_token}"} +) +threads = await remote_graph.threads.list() +``` + +=== "JavaScript Client" + +```javascript +import { Client } from "@langchain/langgraph-sdk"; +// generate token with your auth provider +const my_token = "your-token"; +const client = new Client({ + apiUrl: "http://localhost:2024", + headers: { Authorization: `Bearer ${my_token}` }, +}); +const threads = await client.threads.list(); +``` + +=== "JavaScript RemoteGraph" + +```javascript +import { RemoteGraph } from "@langchain/langgraph/remote"; +// generate token with your auth provider +const my_token = "your-token"; +const remoteGraph = new RemoteGraph({ + graphId: "agent", + url: "http://localhost:2024", + headers: { Authorization: `Bearer ${my_token}` }, +}); +const threads = await remoteGraph.threads.list(); +``` + +=== "CURL" + +```bash +curl -H "Authorization: Bearer your-token" http://localhost:2024/threads +``` diff --git a/docs/docs/how-tos/auth/openapi_security.md b/docs/docs/how-tos/auth/openapi_security.md new file mode 100644 index 000000000..81bb201bd --- /dev/null +++ b/docs/docs/how-tos/auth/openapi_security.md @@ -0,0 +1,95 @@ +# How to document API authentication in OpenAPI + +This guide shows how to customize the OpenAPI security schema for your LangGraph Platform API documentation. This only updates the OpenAPI specification to document your security requirements - to implement the actual authentication logic, see [How to add custom authentication](./custom_auth.md). + +This guide applies to all LangGraph Platform deployments (Cloud, BYOC, and self-hosted). It does not apply to isolated usage of the LangGraph open source library in your own custom server. + +## Default Schema + +The default security scheme varies by deployment type: + +=== "LangGraph Cloud" + +By default, LangGraph Cloud requires a LangSmith API key in the `x-api-key` header: + +```yaml +components: + securitySchemes: + apiKeyAuth: + type: apiKey + in: header + name: x-api-key +security: + - apiKeyAuth: [] +``` + +When using one of the LangGraph SDK's, this can be inferred from environment variables. + +=== "Self-hosted" + +By default, self-hosted deployments have no security scheme. This means they are to be deployed only on a secured network or with authentication. To add custom authentication, see [How to add custom authentication](./custom_auth.md). + +## Custom Security Schema + +To customize the security schema in your OpenAPI documentation, add an `openapi` field to your `auth` configuration in `langgraph.json`. Remember that this only updates the API documentation - you must also implement the corresponding authentication logic as shown in [How to add custom authentication](./custom_auth.md). + +Note that LangGraph Platform does not provide authentication endpoints - you'll need to handle user authentication in your client application and pass the resulting credentials to the LangGraph API. + +=== "OAuth2 with Bearer Token" + +```json +{ + "auth": { + "path": "./auth.py:my_auth", // Implement auth logic here + "openapi": { + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://your-auth-server.com/oauth/authorize", + "scopes": { + "me": "Read information about the current user", + "threads": "Access to create and manage threads" + } + } + } + } + }, + "security": [ + {"OAuth2": ["me", "threads"]} + ] + } + } +} +``` + +=== "API Key" + +```json +{ + "auth": { + "path": "./auth.py:my_auth", // Implement auth logic here + "openapi": { + "securitySchemes": { + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + }, + "security": [ + {"apiKeyAuth": []} + ] + } + } +} +``` + +## Testing + +After updating your configuration: + +1. Deploy your application +2. Visit `/docs` to see the updated OpenAPI documentation +3. Try out the endpoints using credentials from your authentication server (make sure you've implemented the authentication logic first) diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 193564a67..6cad576ad 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -179,11 +179,10 @@ 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) - +- [How to add custom authentication](./auth/custom_auth_new.md) +- [How to update the security schema of your OpenAPI spec](./auth/openapi_security_new.md) ### Assistants From 12f2a480cdf88cb9db36a6a60b971ce7d18575b6 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:25:48 -0800 Subject: [PATCH 04/29] Add concepts --- docs/docs/concepts/auth.md | 216 +++++++++++++++++ docs/docs/concepts/index.md | 1 + docs/docs/tutorials/auth/getting_started.md | 242 ++++++++++---------- 3 files changed, 338 insertions(+), 121 deletions(-) create mode 100644 docs/docs/concepts/auth.md diff --git a/docs/docs/concepts/auth.md b/docs/docs/concepts/auth.md new file mode 100644 index 000000000..db7c83621 --- /dev/null +++ b/docs/docs/concepts/auth.md @@ -0,0 +1,216 @@ +# Authentication & Access Control + +LangGraph Platform provides a flexible authentication and authorization system that can integrate with most authentication schemes. This guide explains the core concepts and how they work together. + +!!! note "Python only" +We currently only support custom authentication and authorization in Python deployments. Support for LangGraph.JS will be added soon. + +## Core Concepts + +### Authentication vs Authorization + +While often used interchangeably, these terms represent distinct security concepts: + +- **Authentication** ("AuthN") verifies _who_ you are. This runs as middleware for every request. +- **Authorization** ("AuthZ") determines _what you can do_. This validates the user's privileges and roles on a per-resource basis. + +In LangGraph Platform, authentication is handled by your `@auth.authenticate` handler, and authorization is handled by your `@auth.on` handlers. + +## Authentication + +Authentication in LangGraph runs as middleware on every request. Your `@auth.authenticate` handler receives request information and must: + +1. Validate the credentials +2. Return user information if valid +3. Raise an HTTP exception if invalid (or AssertionError) + +```python +from langgraph_sdk import Auth + +auth = Auth() + +@auth.authenticate +async def authenticate(headers: dict) -> Auth.types.MinimalUserDict: + # Validate credentials (e.g., API key, JWT token) + api_key = headers.get("x-api-key") + if not api_key or not is_valid_key(api_key): + raise Auth.exceptions.HTTPException( + status_code=401, + detail="Invalid API key" + ) + + # Return user info - only identity and is_authenticated are required + # Add any additional fields you need for authorization + return { + "identity": "user-123", # Required: unique user identifier + "is_authenticated": True, # Optional: assumed True by default + "permissions": ["read", "write"] # Optional: for permission-based auth + # You can add more custom fields if you want to implement other auth patterns + "role": "admin", + "org_id": "org-456" + + } +``` + +The returned user information is available: + +- To your authorization handlers via `ctx.user` +- In your application via `config["configuration"]["langgraph_auth_user"]` + +## Authorization + +After authentication, LangGraph calls your `@auth.on` handlers to control access to specific resources (e.g., threads, assistants, crons). These handlers can: + +1. Add metadata to be saved during resource creation by mutating the `value["metadata"]` dictionary directly. +2. Filter resources by metadata during search/list or read operations by returning a filter dictionary. +3. Raise an HTTP exception if access is denied. + +If you want to just implement simple user-scoped access control, you can use a single `@auth.on` handler for all resources and actions. + +```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 +``` + +### Resource-Specific Handlers + +You can register handlers for specific resources and actions using the `@auth.on` decorator. +When a request is made, the most specific handler that matches that resource and action is called. + +```python +# Generic / global handler catches calls that aren't handled by more specific handlers +@auth.on +async def reject_unhandled_requests(ctx: Auth.types.AuthContext, value: Any) -> None: + print(f"Request to {ctx.path} by {ctx.user.identity}") + return False + +# Thread creation +@auth.on.threads.create +async def on_thread_create( + ctx: Auth.types.AuthContext, + value: Auth.types.threads.create.value +): + metadata = value.setdefault("metadata", {}) + metadata["owner"] = ctx.user.identity + return {"owner": ctx.user.identity} + +# Thread retrieval +@auth.on.threads.read +async def on_thread_read( + ctx: Auth.types.AuthContext, + value: Auth.types.threads.read.value +): + return {"owner": ctx.user.identity} + +# Run creation, streaming, updates, etc. +@auth.on.threads.create_run +async def on_run_create( + ctx: Auth.types.AuthContext, + value: Auth.types.threads.create_run.value +): + # Inherit thread's access control + return {"owner": ctx.user.identity} + +# Assistant creation +@auth.on.assistants.create +async def on_assistant_create( + ctx: Auth.types.AuthContext, + value: Auth.types.assistants.create.value +): + if "admin" not in ctx.user.get("role", []): + raise Auth.exceptions.HTTPException( + status_code=403, + detail="Only admins can create assistants" + ) +``` + +Using the setup above, a request to create a `thread` would match the `on_thread_create` handler, since it is the most specific handler for that resource and action. A request to create a `cron`, on the other hand, would match the global handler, since no more specific handler is registered for that resource and action. + +### Filter Operations + +Authorization handlers can return a filter dictionary to filter resources during all operations (both reads and writes). The filter dictionary supports two additional operators: + +- `$eq`: Exact match (e.g., `{"owner": {"$eq": user_id}}`) - this is equivalent to `{"owner": user_id}` +- `$contains`: List membership (e.g., `{"allowed_users": {"$contains": user_id}}`) + +A dictionary with multiple keys is converted to a logical `AND` filter. For example, `{"owner": user_id, "org_id": org_id}` is converted to `{"$and": [{"owner": user_id}, {"org_id": org_id}]}` + +## Common Access Patterns + +Here are some typical authorization patterns: + +### Single-Owner Resources + +```python +@auth.on +async def owner_only(ctx: Auth.types.AuthContext, value: dict): + metadata = value.setdefault("metadata", {}) + metadata["owner"] = ctx.user.identity + return {"owner": ctx.user.identity} +``` + +### Permission-based Access + +```python +# In your auth handler: +@auth.authenticate +async def authenticate(headers: dict) -> Auth.types.MinimalUserDict: + ... + return { + "identity": "user-123", + "is_authenticated": True, + "permissions": ["threads:write", "threads:read"] # Define permissions in auth + } + +def _default(ctx: Auth.types.AuthContext, value: dict): + metadata = value.setdefault("metadata", {}) + metadata["owner"] = ctx.user.identity + return {"owner": ctx.user.identity} + +@auth.on.threads.create +async def create_thread(ctx: Auth.types.AuthContext, value: dict): + if "threads:write" not in ctx.permissions: + raise Auth.exceptions.HTTPException( + status_code=403, + detail="Unauthorized" + ) + return _default(ctx, value) + + +@auth.on.threads.read +async def rbac_create(ctx: Auth.types.AuthContext, value: dict): + if "threads:read" not in ctx.permissions and "threads:write" not in ctx.permissions: + raise Auth.exceptions.HTTPException( + status_code=403, + detail="Unauthorized" + ) + return _default(ctx, value) +``` + +## Default Security Models + +LangGraph Platform provides different security defaults: + +### LangGraph Cloud + +- Uses LangSmith API keys by default +- Requires valid API key in `x-api-key` header +- Can be customized with your auth handler + +### Self-Hosted + +- No default authentication +- Complete flexibility to implement your security model +- You control all aspects of authentication and authorization + +## Next Steps + +For implementation details: + +- [Setting up authentication](../tutorials/auth/getting_started.md) +- [Custom auth handlers](../how-tos/auth/custom_auth.md) diff --git a/docs/docs/concepts/index.md b/docs/docs/concepts/index.md index 4d8e5f06f..ea56af5ba 100644 --- a/docs/docs/concepts/index.md +++ b/docs/docs/concepts/index.md @@ -68,6 +68,7 @@ The LangGraph Platform comprises several components that work together to suppor - [Web-hooks](./langgraph_server.md#webhooks): Webhooks allow your running LangGraph application to send data to external services on specific events. - [Cron Jobs](./langgraph_server.md#cron-jobs): Cron jobs are a way to schedule tasks to run at specific times in your LangGraph application. - [Double Texting](./double_texting.md): Double texting is a common issue in LLM applications where users may send multiple messages before the graph has finished running. This guide explains how to handle double texting with LangGraph Deploy. +- [Authentication & Access Control](./auth.md): Learn about options for authentication and access control when deploying the LangGraph Platform. ### Deployment Options diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md index 7546a5f74..ffea05cda 100644 --- a/docs/docs/tutorials/auth/getting_started.md +++ b/docs/docs/tutorials/auth/getting_started.md @@ -5,6 +5,14 @@ Let's learn how to add custom authentication to a LangGraph Platform deployment. ??? 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. +!!! note "Prerequisites" + + Before you begin, ensure you have the following: + - [GitHub account](https://github.com/) + - [LangSmith account](https://smith.langchain.com/) + - [Supabase account](https://supabase.com/) + - [Anthropic API key](https://console.anthropic.com/) + ## Understanding authentication flow The key components in a token-based authentication system are: @@ -20,7 +28,6 @@ sequenceDiagram participant User participant AuthServer as Auth Server participant LangGraph - User->>AuthServer: 1. Authenticate (username/password) AuthServer-->>User: 2. Return signed JWT token User->>LangGraph: 3. Request with JWT in header @@ -37,18 +44,120 @@ sequenceDiagram In this tutorial, we'll implement password-based authentication using Supabase as our auth server. -## Setting up the project +## Project Structure -First, clone the example template: +After cloning, you'll see these key files: -```bash -git clone https://github.com/langchain-ai/custom-auth.git -cd custom-auth +```shell +custom-auth/ +├── src/ +│ └── security/ +│ └── auth.py # We'll create this +├── langgraph.json # We'll update this +└── .env.example # Environment variables template ``` -This contains our chatbot code, as well as a custom auth handler (discussed below). +## Setting up authentication -### Configure Supabase +### 1. Create the auth handler + +First, let's create our authentication handler. Create a new file at `src/security/auth.py`: + +```python +import os +import httpx +import jwt +from langgraph_sdk import Auth + +# Load from 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"] + +# Create the auth object we'll use to protect our endpoints +auth = Auth() + +@auth.authenticate +async def get_current_user( + authorization: str | None, # "Bearer " +) -> tuple[list[str], Auth.types.MinimalUserDict]: + """Verify the JWT token and return user info.""" + if not authorization: + raise Auth.exceptions.HTTPException( + status_code=401, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + # Extract and verify JWT token + token = authorization.split(" ", 1)[1] + payload = jwt.decode( + token, + SUPABASE_JWT_SECRET, + algorithms=["HS256"], + audience="authenticated", + ) + + # Double-check with Supabase that token is still valid + 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 ensures only users with valid tokens can access our server. However, all users can still see each other's threads. Let's fix that by adding an authorization filter to the bottom of `auth.py`: + +```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 +``` + +Now when users create threads, their ID is automatically added as the owner, and they can only see threads they own. + +### 2. Configure LangGraph + +Next, tell LangGraph about our auth handler. Open `langgraph.json` and add: + +```json +{ + "auth": { + "path": "src/security/auth.py:auth" + } +} +``` + +This points LangGraph to our `auth` object in the `auth.py` file. + +### 3. Set up environment variables + +Copy the example env file and add your Supabase credentials. To get your Supabase credentials: 1. Create a new project at [supabase.com](https://supabase.com) 2. Go to Project Settings > API to find your project's credentials @@ -58,8 +167,7 @@ This contains our chatbot code, as well as a custom auth handler (discussed belo cp .env.example .env ``` -Add the following to your `.env`: - +Add to your `.env`: ```bash SUPABASE_URL=https://your-project.supabase.co SUPABASE_SERVICE_KEY=your-service-key # aka the service_role secret @@ -67,11 +175,11 @@ 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. +Also note down your project's "anon public" key - we'll use this for client authentication. -### Start the server +### 4. Start the server -Install dependencies and start the LangGraph server: +Install dependencies and start LangGraph: ```bash pip install -U "langgraph-cli[inmem]" && pip install -e . @@ -214,113 +322,6 @@ This demonstrates that: 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 " -) -> 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: @@ -332,7 +333,6 @@ Now that you've set everything up, you can deploy your LangGraph application to 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: From ba56ba1b2a09332cc6a9d6298b27a2a9c0ad647f Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:31:13 -0800 Subject: [PATCH 05/29] Add langgraph reference --- docs/docs/cloud/reference/cli.md | 35 +++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/docs/cloud/reference/cli.md b/docs/docs/cloud/reference/cli.md index cf42d533a..004103533 100644 --- a/docs/docs/cloud/reference/cli.md +++ b/docs/docs/cloud/reference/cli.md @@ -25,10 +25,11 @@ The LangGraph command line interface includes commands to build and run a LangGr The LangGraph CLI requires a JSON configuration file with the following keys: -| Key | Description | +| Key | Description | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `dependencies` | **Required**. Array of dependencies for LangGraph Cloud API server. Dependencies can be one of the following: (1) `"."`, which will look for local Python packages, (2) `pyproject.toml`, `setup.py` or `requirements.txt` in the app directory `"./local_package"`, or (3) a package name. | | `graphs` | **Required**. Mapping from graph ID to path where the compiled graph or a function that makes a graph is defined. Example:
  • `./your_package/your_file.py:variable`, where `variable` is an instance of `langgraph.graph.state.CompiledStateGraph`
  • `./your_package/your_file.py:make_graph`, where `make_graph` is a function that takes a config dictionary (`langchain_core.runnables.RunnableConfig`) and creates an instance of `langgraph.graph.state.StateGraph` / `langgraph.graph.state.CompiledStateGraph`.
| +| `auth` | _(Added in v0.0.11)_ Auth configuration containing the path to your authentication handler. Example: `./your_package/auth.py:auth`, where `auth` is an instance of `langgraph_sdk.Auth`. See [authentication guide](../../concepts/auth.md) for details. | | `env` | Path to `.env` file or a mapping from environment variable to its value. | | `store` | Configuration for adding semantic search to the BaseStore. Contains the following fields:
  • `index`: Configuration for semantic search indexing with fields:
    • `embed`: Embedding provider (e.g., "openai:text-embedding-3-small") or path to custom embedding function
    • `dims`: Dimension size of the embedding model. Used to initialize the vector table.
    • `fields` (optional): List of fields to index. Defaults to `["$"]`, meaningto index entire documents. Can be specific fields like `["text", "summary", "some.value"]`
| | `python_version` | `3.11` or `3.12`. Defaults to `3.11`. | @@ -120,6 +121,35 @@ def embed_texts(texts: list[str]) -> list[list[float]]: return [[0.1, 0.2, ...] for _ in texts] # dims-dimensional vectors ``` +#### Adding custom authentication + +```json +{ + "dependencies": ["."], + "graphs": { + "chat": "./chat/graph.py:graph" + }, + "auth": { + "path": "./auth.py:auth", + "openapi": { + "securitySchemes": { + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + }, + "security": [ + {"apiKeyAuth": []} + ] + }, + "disable_studio_auth": false + } +} +``` + +See the [authentication conceptual guide](../../concepts/auth.md) for details, and the [setting up custom authentication](../../tutorials/auth/getting_started.md) guide for a practical walk through of the process. + ## Commands The base command for the LangGraph CLI is `langgraph`. @@ -257,5 +287,4 @@ RUN set -ex && \ RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* -ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}' -``` \ No newline at end of file +ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}' \ No newline at end of file From 7904fdc9284cd02f4001f97a4eb55af7ab51b06f Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:41:00 -0800 Subject: [PATCH 06/29] Update explanations --- docs/docs/concepts/auth.md | 77 +++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/docs/docs/concepts/auth.md b/docs/docs/concepts/auth.md index db7c83621..cab998218 100644 --- a/docs/docs/concepts/auth.md +++ b/docs/docs/concepts/auth.md @@ -3,7 +3,8 @@ LangGraph Platform provides a flexible authentication and authorization system that can integrate with most authentication schemes. This guide explains the core concepts and how they work together. !!! note "Python only" -We currently only support custom authentication and authorization in Python deployments. Support for LangGraph.JS will be added soon. + + We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon. ## Core Concepts @@ -16,6 +17,47 @@ While often used interchangeably, these terms represent distinct security concep In LangGraph Platform, authentication is handled by your `@auth.authenticate` handler, and authorization is handled by your `@auth.on` handlers. +## System Architecture + +A typical authentication setup involves three main components: + +1. **Authentication Provider** (Identity Provider/IdP) + - A dedicated service that manages user identities and credentials + - Examples: Auth0, Supabase Auth, Okta, or your own auth server + - Handles user registration, login, password resets, etc. + - Issues tokens (JWT, session tokens, etc.) after successful authentication + +2. **LangGraph Backend** (Resource Server) + - Your LangGraph application that contains business logic and protected resources + - Validates tokens with the auth provider + - Enforces access control based on user identity and permissions + - Never stores user credentials directly + +3. **Client Application** (Frontend) + - Web app, mobile app, or API client + - Collects user credentials and sends to auth provider + - Receives tokens from auth provider + - Includes tokens in requests to LangGraph backend + +Here's how these components typically interact: + +```mermaid +sequenceDiagram + participant Client as Client App + participant Auth as Auth Provider + participant LG as LangGraph Backend + + Client->>Auth: 1. Login (username/password) + Auth-->>Client: 2. Return token + Client->>LG: 3. Request with token + LG->>Auth: 4. Validate token + Auth-->>LG: 5. Confirm validity + Note over LG: 6. Apply access control + LG-->>Client: 7. Return resources +``` + +Your `@auth.authenticate` handler in LangGraph handles steps 4-5, while your `@auth.on` handlers implement step 6. + ## Authentication Authentication in LangGraph runs as middleware on every request. Your `@auth.authenticate` handler receives request information and must: @@ -192,6 +234,39 @@ async def rbac_create(ctx: Auth.types.AuthContext, value: dict): return _default(ctx, value) ``` +## Supported Resources + +LangGraph provides authorization handlers for the following resource types: + +### Threads +- `@auth.on.threads.create` - Thread creation +- `@auth.on.threads.read` - Thread retrieval +- `@auth.on.threads.update` - Thread updates +- `@auth.on.threads.delete` - Thread deletion +- `@auth.on.threads.search` - Listing threads + +**Runs:** are scoped to their parent thread for access control. This means permissions are typically inherited from the thread, reflecting the conversational nature of the data model. + +- `@auth.on.threads.create_run` - Creating or updating a run + +All other run operations (reading, listing) are controlled by the thread's handlers, since runs are always accessed in the context of their thread. + +### Assistants +- `@auth.on.assistants.create` - Assistant creation +- `@auth.on.assistants.read` - Assistant retrieval +- `@auth.on.assistants.update` - Assistant updates +- `@auth.on.assistants.delete` - Assistant deletion +- `@auth.on.assistants.search` - Listing assistants + +### Crons +- `@auth.on.crons.create` - Cron job creation +- `@auth.on.crons.read` - Cron job retrieval +- `@auth.on.crons.update` - Cron job updates +- `@auth.on.crons.delete` - Cron job deletion +- `@auth.on.crons.search` - Listing cron jobs + +You can also use the global `@auth.on` handler to implement a single access control policy across all resources and actions, or resource level `@auth.on.threads`, etc. handlers to implement control over all actions of a single resource. + ## Default Security Models LangGraph Platform provides different security defaults: From 97b3d1b9ac1ed30c7a950fc9662fa4819fb1ed51 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:36:03 -0800 Subject: [PATCH 07/29] Simplify tutorial --- docs/docs/how-tos/auth/custom_auth.md | 70 ++++++++++---------- docs/docs/how-tos/index.md | 4 +- docs/docs/tutorials/auth/getting_started.md | 73 +++++++-------------- docs/docs/tutorials/index.md | 9 +-- docs/mkdocs.yml | 7 +- 5 files changed, 70 insertions(+), 93 deletions(-) diff --git a/docs/docs/how-tos/auth/custom_auth.md b/docs/docs/how-tos/auth/custom_auth.md index 872c3c216..e49c23091 100644 --- a/docs/docs/how-tos/auth/custom_auth.md +++ b/docs/docs/how-tos/auth/custom_auth.md @@ -76,47 +76,47 @@ threads = await client.threads.list() === "Python RemoteGraph" -```python -from langgraph.pregel.remote import RemoteGraph -# generate token with your auth provider -my_token = "your-token" -remote_graph = RemoteGraph( - "agent", - url="http://localhost:2024", - headers={"Authorization": f"Bearer {my_token}"} -) -threads = await remote_graph.threads.list() -``` + ```python + from langgraph.pregel.remote import RemoteGraph + # generate token with your auth provider + my_token = "your-token" + remote_graph = RemoteGraph( + "agent", + url="http://localhost:2024", + headers={"Authorization": f"Bearer {my_token}"} + ) + threads = await remote_graph.threads.list() + ``` === "JavaScript Client" -```javascript -import { Client } from "@langchain/langgraph-sdk"; -// generate token with your auth provider -const my_token = "your-token"; -const client = new Client({ - apiUrl: "http://localhost:2024", - headers: { Authorization: `Bearer ${my_token}` }, -}); -const threads = await client.threads.list(); -``` + ```javascript + import { Client } from "@langchain/langgraph-sdk"; + // generate token with your auth provider + const my_token = "your-token"; + const client = new Client({ + apiUrl: "http://localhost:2024", + headers: { Authorization: `Bearer ${my_token}` }, + }); + const threads = await client.threads.list(); + ``` === "JavaScript RemoteGraph" -```javascript -import { RemoteGraph } from "@langchain/langgraph/remote"; -// generate token with your auth provider -const my_token = "your-token"; -const remoteGraph = new RemoteGraph({ - graphId: "agent", - url: "http://localhost:2024", - headers: { Authorization: `Bearer ${my_token}` }, -}); -const threads = await remoteGraph.threads.list(); -``` + ```javascript + import { RemoteGraph } from "@langchain/langgraph/remote"; + // generate token with your auth provider + const my_token = "your-token"; + const remoteGraph = new RemoteGraph({ + graphId: "agent", + url: "http://localhost:2024", + headers: { Authorization: `Bearer ${my_token}` }, + }); + const threads = await remoteGraph.threads.list(); + ``` === "CURL" -```bash -curl -H "Authorization: Bearer your-token" http://localhost:2024/threads -``` + ```bash + curl -H "Authorization: Bearer your-token" http://localhost:2024/threads + ``` diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index 6cad576ad..0f836fe57 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -181,8 +181,8 @@ LangGraph applications can be deployed using LangGraph Cloud, which provides a r ### Authentication & Access Control -- [How to add custom authentication](./auth/custom_auth_new.md) -- [How to update the security schema of your OpenAPI spec](./auth/openapi_security_new.md) +- [How to add custom authentication](./auth/custom_auth.md) +- [How to update the security schema of your OpenAPI spec](./auth/openapi_security.md) ### Assistants diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md index ffea05cda..783cf29cc 100644 --- a/docs/docs/tutorials/auth/getting_started.md +++ b/docs/docs/tutorials/auth/getting_started.md @@ -1,9 +1,9 @@ # Setting up custom authentication -Let's learn how to add custom authentication to a LangGraph Platform deployment. We'll cover the core concepts of token-based authentication and show how to integrate with an authentication server. +Let's add custom authentication to a LangGraph template. This lets users interact with our bot making their conversations accessible to other users. This tutorial covers 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. +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. !!! note "Prerequisites" @@ -21,58 +21,35 @@ The key components in a token-based authentication system are: 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: - -```mermaid -sequenceDiagram - participant User - participant AuthServer as Auth Server - participant LangGraph - User->>AuthServer: 1. Authenticate (username/password) - AuthServer-->>User: 2. Return signed JWT token - User->>LangGraph: 3. Request with JWT in header - LangGraph->>AuthServer: 4a. Validate token - AuthServer-->>LangGraph: 4b. Confirm token validity - Note over LangGraph: 4c. Apply access filters - LangGraph-->>User: Return authorized resources -``` +Here's how it typically works: 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. - -## Project Structure +## 1. Clone the template -After cloning, you'll see these key files: +Clone the [LangGraph template](https://github.com/langchain-ai/new-langgraph-project) to get started. ```shell -custom-auth/ -├── src/ -│ └── security/ -│ └── auth.py # We'll create this -├── langgraph.json # We'll update this -└── .env.example # Environment variables template +pip install -U "langgraph-cli[inmem]" +langgraph new --template=new-langgraph-project-python custom-auth +cd custom-auth ``` -## Setting up authentication - -### 1. Create the auth handler +### 2. Create the auth handler -First, let's create our authentication handler. Create a new file at `src/security/auth.py`: +Next, let's create our authentication handler. Create a new file at `src/security/auth.py`: ```python import os import httpx -import jwt from langgraph_sdk import Auth # Load from 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"] # Create the auth object we'll use to protect our endpoints auth = Auth() @@ -90,20 +67,14 @@ async def get_current_user( ) try: - # Extract and verify JWT token - token = authorization.split(" ", 1)[1] - payload = jwt.decode( - token, - SUPABASE_JWT_SECRET, - algorithms=["HS256"], - audience="authenticated", - ) - - # Double-check with Supabase that token is still valid + # Fetch the user info from Supabase async with httpx.AsyncClient() as client: response = await client.get( f"{SUPABASE_URL}/auth/v1/user", - headers={"Authorization": f"Bearer {token}"}, + headers={ + "Authorization": authorization, + "apiKey": SUPABASE_SERVICE_KEY, + }, ) if response.status_code != 200: raise Auth.exceptions.HTTPException( @@ -112,7 +83,7 @@ async def get_current_user( ) user_data = response.json() - return [], { + return { "identity": user_data["id"], "display_name": user_data.get("name"), "is_authenticated": True, @@ -147,9 +118,9 @@ Next, tell LangGraph about our auth handler. Open `langgraph.json` and add: ```json { - "auth": { - "path": "src/security/auth.py:auth" - } + "auth": { + "path": "src/security/auth.py:auth" + } } ``` @@ -168,21 +139,21 @@ cp .env.example .env ``` Add 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 ``` -Also note down your project's "anon public" key - we'll use this for client authentication. +Also note down your project's "anon public" key - we'll use this for client authentication below. ### 4. Start the server Install dependencies and start LangGraph: ```bash -pip install -U "langgraph-cli[inmem]" && pip install -e . +pip install -e . langgraph dev --no-browser ``` diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md index 02a7983d2..ffd11fd1c 100644 --- a/docs/docs/tutorials/index.md +++ b/docs/docs/tutorials/index.md @@ -8,14 +8,15 @@ title: Tutorials New to LangGraph or LLM app development? Read this material to get up and running building your first applications. -## Get Started +## Get Started 🚀 {#quick-start} - [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 REST API and LangGraph Studio Web UI. - [LangGraph Template Quickstart](../concepts/template_applications.md): Start building with LangGraph Platform using a template application. - [Deploy with LangGraph Cloud Quickstart](../cloud/quick_start.md): Deploy a LangGraph app using LangGraph Cloud. -## Use cases +## Use cases 🛠️ {#use-cases} + Explore practical implementations tailored for specific scenarios: @@ -72,10 +73,10 @@ Explore practical implementations tailored for specific scenarios: - [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 +## LangGraph Platform 🧱 {#platform} ### Authentication & Access Control -Learn how to secure your LangGraph applications: +Add custom authentication and authorization to your LangGraph Platform deployment. - [Setting Up Custom Authentication](./auth/getting_started.md): Implement OAuth2 authentication to authorize users on your deployment \ No newline at end of file diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 1fb71d807..b661ebe14 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -141,7 +141,8 @@ nav: - tutorials/web-navigation/web_voyager.ipynb - tutorials/usaco/usaco.ipynb - tutorials/extraction/retries.ipynb - - LangGarph Platform: + - LangGraph Platform: + - LangGraph Platform: concepts#langgraph-platform - docs/docs/tutorials/auth/getting_started.md - How-to Guides: @@ -241,6 +242,10 @@ nav: - cloud/deployment/cloud.md - how-tos/deploy-self-hosted.md - how-tos/use-remote-graph.md + - Authentication & Access Control: + - Authentication & Access Control: how-tos#authentication-access-control + - cloud/how-tos/auth/custom_auth_new.md + - cloud/how-tos/auth/openapi_security_new.md - Assistants: - Assistants: how-tos#assistants - cloud/how-tos/configuration_cloud.md From 1c97bddc146e4d75639150ad92e8e1a71c43d3d1 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Tue, 17 Dec 2024 18:09:02 -0800 Subject: [PATCH 08/29] Fix cross linking --- docs/docs/how-tos/auth/custom_auth.md | 103 +++++++------ docs/docs/tutorials/auth/getting_started.md | 153 +++++++++++--------- 2 files changed, 142 insertions(+), 114 deletions(-) diff --git a/docs/docs/how-tos/auth/custom_auth.md b/docs/docs/how-tos/auth/custom_auth.md index e49c23091..8f9fd77cf 100644 --- a/docs/docs/how-tos/auth/custom_auth.md +++ b/docs/docs/how-tos/auth/custom_auth.md @@ -2,7 +2,18 @@ This guide shows how to add custom authentication to your LangGraph Platform application. This guide applies to both LangGraph Cloud, BYOC, and self-hosted deployments. It does not apply to isolated usage of the LangGraph open source library in your own custom server. - +!!! tip "Prerequisites" + + This guide assumes familiarity with the following concepts: + + * [**Authentication & Access Control**](../../concepts/auth.md) + * [**LangGraph Platform**](../../concepts/index.md#langgraph-platform) + + For a more guided walkthrough, see [**setting up custom authentication**](../../tutorials/auth/getting_started.md) tutorial. + +!!! note "Python only" + + We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon. ## 1. Implement authentication @@ -63,60 +74,60 @@ Assuming you are using JWT token authentication, you could access your deploymen === "Python Client" -```python -from langgraph_sdk import get_client -# generate token with your auth provider -my_token = "your-token" -client = get_client( - url="http://localhost:2024", - headers={"Authorization": f"Bearer {my_token}"} -) -threads = await client.threads.list() -``` + ```python + from langgraph_sdk import get_client + + my_token = "your-token" # In practice, you would generate a signed token with your auth provider + client = get_client( + url="http://localhost:2024", + headers={"Authorization": f"Bearer {my_token}"} + ) + threads = await client.threads.list() + ``` === "Python RemoteGraph" - ```python - from langgraph.pregel.remote import RemoteGraph - # generate token with your auth provider - my_token = "your-token" - remote_graph = RemoteGraph( - "agent", - url="http://localhost:2024", - headers={"Authorization": f"Bearer {my_token}"} - ) - threads = await remote_graph.threads.list() - ``` + ```python + from langgraph.pregel.remote import RemoteGraph + + my_token = "your-token" # In practice, you would generate a signed token with your auth provider + remote_graph = RemoteGraph( + "agent", + url="http://localhost:2024", + headers={"Authorization": f"Bearer {my_token}"} + ) + threads = await remote_graph.threads.list() + ``` === "JavaScript Client" - ```javascript - import { Client } from "@langchain/langgraph-sdk"; - // generate token with your auth provider - const my_token = "your-token"; - const client = new Client({ - apiUrl: "http://localhost:2024", - headers: { Authorization: `Bearer ${my_token}` }, - }); - const threads = await client.threads.list(); - ``` + ```javascript + import { Client } from "@langchain/langgraph-sdk"; + + const my_token = "your-token"; // In practice, you would generate a signed token with your auth provider + const client = new Client({ + apiUrl: "http://localhost:2024", + headers: { Authorization: `Bearer ${my_token}` }, + }); + const threads = await client.threads.list(); + ``` === "JavaScript RemoteGraph" - ```javascript - import { RemoteGraph } from "@langchain/langgraph/remote"; - // generate token with your auth provider - const my_token = "your-token"; - const remoteGraph = new RemoteGraph({ - graphId: "agent", - url: "http://localhost:2024", - headers: { Authorization: `Bearer ${my_token}` }, - }); - const threads = await remoteGraph.threads.list(); - ``` + ```javascript + import { RemoteGraph } from "@langchain/langgraph/remote"; + + const my_token = "your-token"; // In practice, you would generate a signed token with your auth provider + const remoteGraph = new RemoteGraph({ + graphId: "agent", + url: "http://localhost:2024", + headers: { Authorization: `Bearer ${my_token}` }, + }); + const threads = await remoteGraph.threads.list(); + ``` === "CURL" - ```bash - curl -H "Authorization: Bearer your-token" http://localhost:2024/threads - ``` + ```bash + curl -H "Authorization: Bearer ${your-token}" http://localhost:2024/threads + ``` diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md index 783cf29cc..5b5edac2d 100644 --- a/docs/docs/tutorials/auth/getting_started.md +++ b/docs/docs/tutorials/auth/getting_started.md @@ -1,19 +1,29 @@ # Setting up custom authentication -Let's add custom authentication to a LangGraph template. This lets users interact with our bot making their conversations accessible to other users. This tutorial covers the core concepts of token-based authentication and show how to integrate with an authentication server. +Let's add OAuth2 token authentication to a LangGraph template. This lets users interact with our bot making their conversations accessible to other users. This tutorial covers 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. +???+ tip "Prerequisites" +This guide assumes familiarity with the following concepts: + + * The [LangGraph Platform](../../concepts/index.md#langgraph-platform) + * [Authentication & Access Control](../../concepts/auth.md) in the LangGraph Platform -!!! note "Prerequisites" Before you begin, ensure you have the following: - - [GitHub account](https://github.com/) - - [LangSmith account](https://smith.langchain.com/) - - [Supabase account](https://supabase.com/) - - [Anthropic API key](https://console.anthropic.com/) -## Understanding authentication flow + * [GitHub account](https://github.com/) + * [LangSmith account](https://smith.langchain.com/) + * [Supabase account](https://supabase.com/) + * [Anthropic API key](https://console.anthropic.com/) + +??? note "Python only" + + We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon. + +??? tip "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. + +## Overview The key components in a token-based authentication system are: @@ -21,13 +31,14 @@ The key components in a token-based authentication system are: 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. -Here's how it typically works: +For a typical interaction: 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. + ## 1. Clone the template Clone the [LangGraph template](https://github.com/langchain-ai/new-langgraph-project) to get started. @@ -38,20 +49,50 @@ langgraph new --template=new-langgraph-project-python custom-auth cd custom-auth ``` -### 2. Create the auth handler +### 2. Set up environment variables + +Copy the example `.env` file and add your Supabase credentials. + +```bash +cp .env.example .env +``` + +Add to your `.env`: + +```bash +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_SERVICE_KEY=your-service-key # aka the service_role secret +ANTHROPIC_API_KEY=your-anthropic-key # For the LLM in our chatbot +``` + +To get your Supabase credentials: -Next, let's create our authentication handler. Create a new file at `src/security/auth.py`: +1. Create a project at [supabase.com](https://supabase.com) +2. Go to Project Settings > API +3. Add these credentials to your `.env` file: + +Also note down your project's "anon public" key. We'll use this for client authentication below. + +### 3. Create the auth handler + +Now we'll create an authentication handler that does two things: +1. Authenticates users by validating their tokens (`@auth.authenticate`) +2. Controls what resources those users can access (`@auth.on`) + +We'll use the `Auth` class from `langgraph_sdk` to register these handler functions. The LangGraph backend will automatically call these functions that you've registered whenever a user makes a request. + +Create a new file at `src/security/auth.py`: ```python import os import httpx from langgraph_sdk import Auth -# Load from your .env file +# These will be loaded from your .env file in the next step SUPABASE_URL = os.environ["SUPABASE_URL"] SUPABASE_SERVICE_KEY = os.environ["SUPABASE_SERVICE_KEY"] -# Create the auth object we'll use to protect our endpoints +# The auth handler registers functions that the LangGraph backend will call auth = Auth() @auth.authenticate @@ -59,13 +100,6 @@ async def get_current_user( authorization: str | None, # "Bearer " ) -> tuple[list[str], Auth.types.MinimalUserDict]: """Verify the JWT token and return user info.""" - if not authorization: - raise Auth.exceptions.HTTPException( - status_code=401, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer"}, - ) - try: # Fetch the user info from Supabase async with httpx.AsyncClient() as client: @@ -76,12 +110,7 @@ async def get_current_user( "apiKey": SUPABASE_SERVICE_KEY, }, ) - if response.status_code != 200: - raise Auth.exceptions.HTTPException( - status_code=401, - detail="Invalid token" - ) - + assert response.status_code == 200 user_data = response.json() return { "identity": user_data["id"], @@ -95,7 +124,11 @@ async def get_current_user( ) ``` -This handler ensures only users with valid tokens can access our server. However, all users can still see each other's threads. Let's fix that by adding an authorization filter to the bottom of `auth.py`: +This handler validates the user's information, but by itself doesn't restrict what authenticated users can access. Let's add an authorization handler to limit access to resources. We'll do this by: +1. Adding the user's ID to resource metadata when they create something +2. Using that metadata to filter what resources they can see + +Register this authorization handler with the `@auth.on` decorator. This function will run on all calls that make it past the authentication stage. ```python @auth.on @@ -110,11 +143,11 @@ async def add_owner( return filters ``` -Now when users create threads, their ID is automatically added as the owner, and they can only see threads they own. +Now when users create threads, assistants, runs, or other resources, their ID is automatically added as the owner in its metadata, and they can only see the threads they own. -### 2. Configure LangGraph +### 3. Configure `langgraph.json` -Next, tell LangGraph about our auth handler. Open `langgraph.json` and add: +Next, we need to tell LangGraph that we've created an auth handler. Open `langgraph.json` and add: ```json { @@ -126,40 +159,23 @@ Next, tell LangGraph about our auth handler. Open `langgraph.json` and add: This points LangGraph to our `auth` object in the `auth.py` file. -### 3. Set up environment variables - -Copy the example env file and add your Supabase credentials. To get your Supabase credentials: - -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 to your `.env`: - -```bash -SUPABASE_URL=https://your-project.supabase.co -SUPABASE_SERVICE_KEY=your-service-key # aka the service_role secret -ANTHROPIC_API_KEY=your-anthropic-key # For the LLM in our chatbot -``` - -Also note down your project's "anon public" key - we'll use this for client authentication below. - ### 4. Start the server Install dependencies and start LangGraph: -```bash +```shell 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. "myemail+1@gmail.com" and "myemail+2@gmail.com". +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. + +!!! tip "Multiple example emails" +You can create multiple users with a shared email bya dding a "+" to the email address. For example, "myemail@gmail.com" can be used to create "myemail+1@gmail.com" and "myemail+2@gmail.com". + +Copy the code below. Make sure to fill out the Supabase URL & anon key, as well as the email addresses for your test users. Then run the code. ```python import os @@ -168,9 +184,7 @@ import dotenv from langgraph_sdk import get_client -dotenv.load_dotenv() - -supabase_url: str = os.environ.get("SUPABASE_URL") +supabase_url: str = "CHANGEME" 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 @@ -256,7 +270,9 @@ 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: +We were able to create a thread and have a conversation with the bot. Great! + +Now let's see what happens when we try to access the server without authentication: ```python # Try to access without a token @@ -267,7 +283,9 @@ 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: +Without an authentication token, we couldn't create a new thread! + +If we try to access a thread owned by another user, we'll get an error: ```python # Log in as user 2 @@ -280,6 +298,9 @@ user_2_client = get_client( headers={"Authorization": f"Bearer {user_2_token}"} ) +# This passes +thread2 = await unauthenticated_client.threads.create() + # Try to access user 1's thread try: await user_2_client.threads.get(thread["thread_id"]) @@ -287,10 +308,10 @@ except Exception as e: print(f"Failed to access other user's thread: {e}") # Will show 404 Not Found ``` -This demonstrates that: +Notice 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 +2. Without a token, we get an authentication error saying we are forbidden 3. Even with a valid token, users can only access their own threads ## Deploying to LangGraph Cloud @@ -302,12 +323,8 @@ Now that you've set everything up, you can deploy your LangGraph application to 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. +Once deployed, you should be able to run the client code above again, 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 +Now that you understand token-based authentication, you can try integrating this in actual frontend code! You can see a longer example of this tutorial at the [custom auth template](https://github.com/langchain-ai/custom-auth). There, you can see a full end-to-end example of adding custom authentication to a LangGraph chatbot using a react web frontend. \ No newline at end of file From b85c9961d7280062714d124ec649b168c42bcbc2 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 00:50:38 -0800 Subject: [PATCH 09/29] Split into 3 --- .../cloud/reference/sdk/python_sdk_ref.md | 1 - docs/docs/tutorials/auth/getting_started.md | 367 +++++------------- docs/docs/tutorials/index.md | 6 +- docs/mkdocs.yml | 2 + 4 files changed, 103 insertions(+), 273 deletions(-) diff --git a/docs/docs/cloud/reference/sdk/python_sdk_ref.md b/docs/docs/cloud/reference/sdk/python_sdk_ref.md index 4126ca306..3fde9e9cb 100644 --- a/docs/docs/cloud/reference/sdk/python_sdk_ref.md +++ b/docs/docs/cloud/reference/sdk/python_sdk_ref.md @@ -7,7 +7,6 @@ ::: langgraph_sdk.schema handler: python - ::: langgraph_sdk.auth handler: python diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md index 5b5edac2d..7517b3058 100644 --- a/docs/docs/tutorials/auth/getting_started.md +++ b/docs/docs/tutorials/auth/getting_started.md @@ -1,153 +1,80 @@ -# Setting up custom authentication +# Setting up Custom Authentication -Let's add OAuth2 token authentication to a LangGraph template. This lets users interact with our bot making their conversations accessible to other users. This tutorial covers the core concepts of token-based authentication and show how to integrate with an authentication server. +In this tutorial, we will build a chatbot that only lets specific users access it. We'll start with the LangGraph template and add token-based security step by step. By the end, you'll have a working chatbot that checks for valid tokens before allowing access. -???+ tip "Prerequisites" -This guide assumes familiarity with the following concepts: +!!! note "This is part 1 of our authentication series:" - * The [LangGraph Platform](../../concepts/index.md#langgraph-platform) - * [Authentication & Access Control](../../concepts/auth.md) in the LangGraph Platform + 1. Basic Authentication (you are here) - Control who can access your bot + 2. [Resource Authorization](resource_auth.md) - Let users have private conversations + 3. [Production Auth](supabase_auth.md) - Add real user accounts and validate using OAuth2 +## Setting up our project - Before you begin, ensure you have the following: +First, let's create a new chatbot using the LangGraph starter template: - * [GitHub account](https://github.com/) - * [LangSmith account](https://smith.langchain.com/) - * [Supabase account](https://supabase.com/) - * [Anthropic API key](https://console.anthropic.com/) - -??? note "Python only" - - We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon. - -??? tip "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. - -## Overview - -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. - -For a typical interaction: - -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. - - -## 1. Clone the template - -Clone the [LangGraph template](https://github.com/langchain-ai/new-langgraph-project) to get started. - -```shell +```bash pip install -U "langgraph-cli[inmem]" langgraph new --template=new-langgraph-project-python custom-auth cd custom-auth ``` -### 2. Set up environment variables - -Copy the example `.env` file and add your Supabase credentials. - -```bash -cp .env.example .env -``` - -Add to your `.env`: - -```bash -SUPABASE_URL=https://your-project.supabase.co -SUPABASE_SERVICE_KEY=your-service-key # aka the service_role secret -ANTHROPIC_API_KEY=your-anthropic-key # For the LLM in our chatbot +The template gives us a placeholder LangGraph app. Let's try it out by installing the local dependencies and running the development server. +```shell +pip install -e . +langgraph dev ``` +> - 🚀 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 +> +> This in-memory server is designed for development and testing. +> For production use, please use LangGraph Cloud. -To get your Supabase credentials: +If everything works, the server should start and open the studio in your browser. -1. Create a project at [supabase.com](https://supabase.com) -2. Go to Project Settings > API -3. Add these credentials to your `.env` file: +Now that we've seen the base LangGraph app, let's add authentication to it! -Also note down your project's "anon public" key. We'll use this for client authentication below. +## Adding Authentication -### 3. Create the auth handler +The `Auth` object lets you register an authentication function that the LangGraph platform will run on every request. This function receives each request and decides whether to accept or reject. -Now we'll create an authentication handler that does two things: -1. Authenticates users by validating their tokens (`@auth.authenticate`) -2. Controls what resources those users can access (`@auth.on`) - -We'll use the `Auth` class from `langgraph_sdk` to register these handler functions. The LangGraph backend will automatically call these functions that you've registered whenever a user makes a request. - -Create a new file at `src/security/auth.py`: +Create a new file `src/security/auth.py`. This is where we'll our code will live to check if users are allowed to access our bot: ```python -import os -import httpx from langgraph_sdk import Auth -# These will be loaded from your .env file in the next step -SUPABASE_URL = os.environ["SUPABASE_URL"] -SUPABASE_SERVICE_KEY = os.environ["SUPABASE_SERVICE_KEY"] +# This is our toy user database +VALID_TOKENS = { + "user1-token": {"id": "user1", "name": "Alice"}, + "user2-token": {"id": "user2", "name": "Bob"}, +} -# The auth handler registers functions that the LangGraph backend will call auth = Auth() -@auth.authenticate -async def get_current_user( - authorization: str | None, # "Bearer " -) -> tuple[list[str], Auth.types.MinimalUserDict]: - """Verify the JWT token and return user info.""" - try: - # Fetch the user info from Supabase - async with httpx.AsyncClient() as client: - response = await client.get( - f"{SUPABASE_URL}/auth/v1/user", - headers={ - "Authorization": authorization, - "apiKey": SUPABASE_SERVICE_KEY, - }, - ) - assert response.status_code == 200 - 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 validates the user's information, but by itself doesn't restrict what authenticated users can access. Let's add an authorization handler to limit access to resources. We'll do this by: -1. Adding the user's ID to resource metadata when they create something -2. Using that metadata to filter what resources they can see - -Register this authorization handler with the `@auth.on` decorator. This function will run on all calls that make it past the authentication stage. - -```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 +@auth.authenticate +async def get_current_user(authorization: str | None) -> Auth.types.MinimalUserDict: + """Check if the user's token is valid.""" + assert authorization + scheme, token = authorization.split() + assert scheme.lower() == "bearer" + # Check if token is valid + if token not in VALID_TOKENS: + raise Auth.exceptions.HTTPException(status_code=401, detail="Invalid token") + + # Return user info if valid + user_data = VALID_TOKENS[token] + return { + "identity": user_data["id"], + } ``` -Now when users create threads, assistants, runs, or other resources, their ID is automatically added as the owner in its metadata, and they can only see the threads they own. +Notice that our authentication handler does two important things: -### 3. Configure `langgraph.json` +1. Checks if a valid token is provided +2. Returns the user's -Next, we need to tell LangGraph that we've created an auth handler. Open `langgraph.json` and add: +Now tell LangGraph to use our authentication by adding the following to the `langgraph.json` configuration: ```json { @@ -157,174 +84,74 @@ Next, we need to tell LangGraph that we've created an auth handler. Open `langgr } ``` -This points LangGraph to our `auth` object in the `auth.py` file. +## Testing Our Secure Bot -### 4. Start the server +Let's start the server again to test everything out! -Install dependencies and start LangGraph: - -```shell -pip install -e . +```bash langgraph dev --no-browser ``` -## Interacting with the server +??? note "Custom auth in the studio" -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. + If you didn't add the `--no-browser`, the studio UI will open in the browser. You may wonder, how is the studio able to still connect to our server? By default, we also permit access from the LangGraph studio, even when using custom auth. This makes it easier to develop and test your bot in the studio. You can remove this alternative authentication option by + setting `disable_studio_auth: "true"` in your auth configuration: + ```json + { + "auth": { + "path": "src/security/auth.py:auth", + "disable_studio_auth": "true" + } + } + ``` -!!! tip "Multiple example emails" -You can create multiple users with a shared email bya dding a "+" to the email address. For example, "myemail@gmail.com" can be used to create "myemail+1@gmail.com" and "myemail+2@gmail.com". - -Copy the code below. Make sure to fill out the Supabase URL & anon key, as well as the email addresses for your test users. Then run the code. +Now let's try to chat with our bot. Create a new file `test_auth.py`: ```python -import os -import httpx -import dotenv - +import asyncio from langgraph_sdk import get_client -supabase_url: str = "CHANGEME" -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}") -``` - -We were able to create a thread and have a conversation with the bot. Great! +async def test_auth(): + # Try without a token (should fail) + client = get_client(url="http://localhost:2024") + try: + thread = await client.threads.create() + print("❌ Should have failed without token!") + except Exception as e: + print("✅ Correctly blocked access:", e) -Now let's see what happens when we try to access the server without authentication: + # Try with a valid token + client = get_client( + url="http://localhost:2024", headers={"Authorization": "Bearer user1-token"} + ) -```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 -``` + # Create a thread and chat + thread = await client.threads.create() + print(f"✅ Created thread as Alice: {thread['thread_id']}") -Without an authentication token, we couldn't create a new thread! + response = await client.runs.create( + thread_id=thread["thread_id"], + assistant_id="agent", + input={"messages": [{"role": "user", "content": "Hello!"}]}, + ) + print("✅ Bot responded:") + print(response) -If we try to access a thread owned by another user, we'll get an error: -```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}"} -) - -# This passes -thread2 = await unauthenticated_client.threads.create() - -# 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 +if __name__ == "__main__": + asyncio.run(test_auth()) ``` -Notice that: - -1. With a valid token, we can create and interact with threads -2. Without a token, we get an authentication error saying we are forbidden -3. Even with a valid token, users can only access their own threads - -## 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". +Run the test code and you should see that: +1. Without a valid token, we can't access the bot +2. With a valid token, we can create threads and chat -Once deployed, you should be able to run the client code above again, replacing the `http://localhost:2024` with the URL of your deployment. +Congratulations! You've built a chatbot that only lets "authorized" users access it. While this system doesn't (yet) implement a production-ready security scheme, we've learned the basic mechanics of how to control access to our bot. In the next tutorial, we'll learn how to give each user their own private conversations. -## Next steps +## What's Next? -Now that you understand token-based authentication, you can try integrating this in actual frontend code! You can see a longer example of this tutorial at the [custom auth template](https://github.com/langchain-ai/custom-auth). There, you can see a full end-to-end example of adding custom authentication to a LangGraph chatbot using a react web frontend. \ No newline at end of file +Now that you can control who accesses your bot, you might want to: +1. Move on to [Resource Authorization](resource_auth.md) to learn how to make conversations private +2. Read more about [authentication concepts](../../concepts/auth.md) +3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md) for more authentication options \ No newline at end of file diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md index ffd11fd1c..31fcfddbb 100644 --- a/docs/docs/tutorials/index.md +++ b/docs/docs/tutorials/index.md @@ -77,6 +77,8 @@ Explore practical implementations tailored for specific scenarios: ### Authentication & Access Control -Add custom authentication and authorization to your LangGraph Platform deployment. +Add custom authentication and authorization to an existing LangGraph Platform deployment in the following three-part guide: -- [Setting Up Custom Authentication](./auth/getting_started.md): Implement OAuth2 authentication to authorize users on your deployment \ No newline at end of file +1. [Setting Up Custom Authentication](./auth/getting_started.md): Implement OAuth2 authentication to authorize users on your deployment +2. [Resource Authorization](./auth/resource_auth.md): Let users have private conversations +3. [Connecting an Authentication Provider](./auth/supabase_auth.md): Add real user accounts and validate using OAuth2 \ No newline at end of file diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index b661ebe14..09bb726fe 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -144,6 +144,8 @@ nav: - LangGraph Platform: - LangGraph Platform: concepts#langgraph-platform - docs/docs/tutorials/auth/getting_started.md + - docs/docs/tutorials/auth/resource_auth.md + - docs/docs/tutorials/auth/add_auth_server.md - How-to Guides: - how-tos/index.md From 30e647abce45cb882a58cd6b77ca609045e05f4a Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 01:06:01 -0800 Subject: [PATCH 10/29] Add links --- docs/docs/concepts/auth.md | 2 +- docs/docs/tutorials/auth/getting_started.md | 4 ++-- docs/docs/tutorials/index.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docs/concepts/auth.md b/docs/docs/concepts/auth.md index cab998218..54b42cdd2 100644 --- a/docs/docs/concepts/auth.md +++ b/docs/docs/concepts/auth.md @@ -15,7 +15,7 @@ While often used interchangeably, these terms represent distinct security concep - **Authentication** ("AuthN") verifies _who_ you are. This runs as middleware for every request. - **Authorization** ("AuthZ") determines _what you can do_. This validates the user's privileges and roles on a per-resource basis. -In LangGraph Platform, authentication is handled by your `@auth.authenticate` handler, and authorization is handled by your `@auth.on` handlers. +In LangGraph Platform, authentication is handled by your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler, and authorization is handled by your [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) handlers. ## System Architecture diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md index 7517b3058..85910dbd4 100644 --- a/docs/docs/tutorials/auth/getting_started.md +++ b/docs/docs/tutorials/auth/getting_started.md @@ -6,7 +6,7 @@ In this tutorial, we will build a chatbot that only lets specific users access i 1. Basic Authentication (you are here) - Control who can access your bot 2. [Resource Authorization](resource_auth.md) - Let users have private conversations - 3. [Production Auth](supabase_auth.md) - Add real user accounts and validate using OAuth2 + 3. [Production Auth](add_auth_server.md) - Add real user accounts and validate using OAuth2 ## Setting up our project @@ -36,7 +36,7 @@ Now that we've seen the base LangGraph app, let's add authentication to it! ## Adding Authentication -The `Auth` object lets you register an authentication function that the LangGraph platform will run on every request. This function receives each request and decides whether to accept or reject. +The [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object lets you register an authentication function that the LangGraph platform will run on every request. This function receives each request and decides whether to accept or reject. Create a new file `src/security/auth.py`. This is where we'll our code will live to check if users are allowed to access our bot: diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md index 31fcfddbb..be7d56108 100644 --- a/docs/docs/tutorials/index.md +++ b/docs/docs/tutorials/index.md @@ -81,4 +81,4 @@ Add custom authentication and authorization to an existing LangGraph Platform de 1. [Setting Up Custom Authentication](./auth/getting_started.md): Implement OAuth2 authentication to authorize users on your deployment 2. [Resource Authorization](./auth/resource_auth.md): Let users have private conversations -3. [Connecting an Authentication Provider](./auth/supabase_auth.md): Add real user accounts and validate using OAuth2 \ No newline at end of file +3. [Connecting an Authentication Provider](./auth/add_auth_server.md): Add real user accounts and validate using OAuth2 \ No newline at end of file From 8fc3f204eac8ed6ec00bf3310708c79cacc0b77d Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 01:10:33 -0800 Subject: [PATCH 11/29] Add pt 2 and 3 --- docs/docs/tutorials/auth/add_auth_server.md | 337 ++++++++++++++++++++ docs/docs/tutorials/auth/resource_auth.md | 259 +++++++++++++++ 2 files changed, 596 insertions(+) create mode 100644 docs/docs/tutorials/auth/add_auth_server.md create mode 100644 docs/docs/tutorials/auth/resource_auth.md diff --git a/docs/docs/tutorials/auth/add_auth_server.md b/docs/docs/tutorials/auth/add_auth_server.md new file mode 100644 index 000000000..cfc5b86ca --- /dev/null +++ b/docs/docs/tutorials/auth/add_auth_server.md @@ -0,0 +1,337 @@ +# Connecting an Authentication Provider + +In the previous tutorial, we added [resource authorization](../../concepts/auth.md#resource-authorization) to give users private conversations. However, we were still using hard-coded tokens for authentication, which is not secure. Now we'll replace those tokens with real user accounts using [OAuth2](../../concepts/auth.md#oauth2-authentication). + +We'll keep the same [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object and [resource-level access control](../../concepts/auth.md#resource-level-access-control), but upgrade our authentication to use Supabase as our identity provider. While we use Supabase in this tutorial, the concepts apply to any OAuth2 provider. You'll learn how to: + +1. Replace test tokens with real [JWT tokens](../../concepts/auth.md#jwt-tokens) +2. Integrate with OAuth2 providers for secure user authentication +3. Handle user sessions and metadata while maintaining our existing authorization logic + +!!! note "This is part 3 of our authentication series:" + + 1. [Basic Authentication](getting_started.md) - Control who can access your bot + 2. [Resource Authorization](resource_auth.md) - Let users have private conversations + 3. Production Auth (you are here) - Add real user accounts and validate using OAuth2 + +!!! warning "Prerequisites" + + - Complete the [Resource Authorization](resource_auth.md) tutorial + - [Create a Supabase project](https://supabase.com/dashboard) + - Have your project URL and service role key ready + +## Background + +OAuth2 involves three main roles: + +1. **Authorization server**: The identity provider (e.g., Supabase, Auth0, Google) that handles user authentication and issues tokens +2. **Application backend**: Your LangGraph application. This validates tokens and serves protected resources (conversation data) +3. **Client application**: The web or mobile app where users interact with your service + +A standard OAuth2 flow works something like this: + + +```mermaid +sequenceDiagram + participant User + participant Client + participant AuthServer + participant LangGraph Backend + + User->>Client: Initiate login + User->>AuthServer: Enter credentials + AuthServer->>Client: Send tokens + Client->>LangGraph Backend: Request with token + LangGraph Backend->>AuthServer: Validate token + AuthServer->>LangGraph Backend: Token valid + LangGraph Backend->>Client: Serve request (e.g., run agent or graph) +``` + +In the following example, we'll use Supabase as our auth server. The LangGraph application will provide the backend for your app, and we will write test code for the client app. +Let's get started! + +## Setting Up Authentication Provider + +First, let's install the required dependencies. Start in your `custom-auth` directory and ensure you have the `langgraph-cli` installed: + +```bash +cd custom-auth +pip install -U "langgraph-cli[inmem]" +``` + +Next, we'll need to fech the URL of our auth server and the private key for authentication. +Since we're using Supabase for this, we can do this in the Supabase dashboard: + +1. In the left sidebar, click on t️⚙ Project Settings" and then click "API" +2. Copy your project URL and add it to your `.env` file + +```shell +echo "SUPABASE_URL=your-project-url" >> .env +``` + +3. Next, copy your service role secret key and add it to your `.env` file + +```shell +echo "SUPABASE_SERVICE_KEY=your-service-role-key" >> .env +``` + +4. Finally, copy your "anon public" key and note it down. This will be used later when we set up our client code. + +```bash +SUPABASE_URL=your-project-url +SUPABASE_SERVICE_KEY=your-service-role-key +``` + +## Implementing Token Validation + +In the previous tutorials, we used the [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object to: + +1. Validate hard-coded tokens in the [authentication tutorial](getting_started.md) +2. Add resource ownership in the [authorization tutorial](resource_auth.md) + +Now we'll upgrade our authentication to validate real JWT tokens from Supabase. The key changes will all be in the [`@auth.authenticate`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) decorated function: + +1. Instead of checking against a hard-coded list of tokens, we'll make an HTTP request to Supabase to validate the token +2. We'll extract real user information (ID, email) from the validated token + +And we'll keep our existing resource authorization logic unchanged + +Let's update `src/security/auth.py` to implement this: + +```python +import os +import httpx +from langgraph_sdk import Auth + +auth = Auth() + +# This is loaded from the `.env` file you created above +SUPABASE_URL = os.environ["SUPABASE_URL"] +SUPABASE_SERVICE_KEY = os.environ["SUPABASE_SERVICE_KEY"] + + +@auth.authenticate +async def get_current_user(authorization: str | None): + """Validate JWT tokens and extract user information.""" + assert authorization + scheme, token = authorization.split() + assert scheme.lower() == "bearer" + + try: + # Verify token with auth provider + async with httpx.AsyncClient() as client: + response = await client.get( + f"{SUPABASE_URL}/auth/v1/user", + headers={ + "Authorization": authorization, + "apiKey": SUPABASE_SERVICE_KEY, + }, + ) + assert response.status_code == 200 + user = response.json() + return { + "identity": user["id"], # Unique user identifier + "email": user["email"], + "is_authenticated": True, + } + except Exception as e: + raise Auth.exceptions.HTTPException(status_code=401, detail=str(e)) + + +# Keep our resource authorization from the previous tutorial +@auth.on +async def add_owner(ctx, value): + """Make resources private to their creator using resource metadata.""" + filters = {"owner": ctx.user.identity} + metadata = value.setdefault("metadata", {}) + metadata.update(filters) + return filters +``` + +The most important change is that we're now validating tokens with a real authentication server. Our authentication handler has the private key for our Supabase project, which we can use to validate the user's token and extract their information. + +Let's test this with a real user account! + +## Testing Authentication Flow + +Create a new file `create_users.py`. This will stand-in for a frontend that lets users sign up and log in. + +```python +import argparse +import asyncio +import os + +import dotenv +import httpx +from langgraph_sdk import get_client + +dotenv.load_dotenv() + +# Get email from command line +parser = argparse.ArgumentParser() +parser.add_argument("email", help="Your email address for testing") +args = parser.parse_args() + +base_email = args.email.split("@") +email1 = f"{base_email[0]}+1@{base_email[1]}" +email2 = f"{base_email[0]}+2@{base_email[1]}" + +SUPABASE_URL = os.environ["SUPABASE_URL"] +SUPABASE_SERVICE_KEY = os.environ["SUPABASE_SERVICE_KEY"] + + +async def sign_up(email: str, password: str): + """Create a new user account.""" + async with httpx.AsyncClient() as client: + response = await client.post( + f"{SUPABASE_URL}/auth/v1/signup", + json={"email": email, "password": password}, + headers={"apiKey": SUPABASE_SERVICE_KEY}, + ) + assert response.status_code == 200 + return response.json() + +async def main(): + # Create two test users + password = "secure-password" # CHANGEME + print(f"Creating test users: {email1} and {email2}") + await sign_up(email1, password) + await sign_up(email2, password) + +if __name__ == "__main__": + asyncio.run(main()) +``` + +Then run the setup script: + +```shell +python create_users.py CHANGEME@example.com +``` + +!!! tip "About test emails" + We'll create two test accounts by adding "+1" and "+2" to your email. For example, if you use "myemail@gmail.com", we'll create "myemail+1@gmail.com" and "myemail+2@gmail.com". All emails will be delivered to your original address. + +⚠️ Before continuing: Check your email and click both confirmation links. This would normally be handled by your frontend. + +Now let's test that users can only see their own data. Create a new file `test_oauth.py`. This will stand-in for your application's frontend. + +```python +import argparse +import asyncio +import os + +import dotenv +import httpx +from langgraph_sdk import get_client + +dotenv.load_dotenv() + +# Get email from command line +parser = argparse.ArgumentParser() +parser.add_argument("email", help="Your email address for testing") +args = parser.parse_args() + +# Create two test emails from the base email +base_email = args.email.split("@") +email1 = f"{base_email[0]}+1@{base_email[1]}" +email2 = f"{base_email[0]}+2@{base_email[1]}" + +# Initialize auth provider settings +SUPABASE_URL = os.environ["SUPABASE_URL"] +SUPABASE_ANON_KEY = os.environ["SUPABASE_ANON_KEY"] + + +async def login(email: str, password: str): + """Get an access token for an existing user.""" + async with httpx.AsyncClient() as client: + response = await client.post( + f"{SUPABASE_URL}/auth/v1/token?grant_type=password", + json={ + "email": email, + "password": password + }, + headers={ + "apikey": SUPABASE_ANON_KEY, + "Content-Type": "application/json" + }, + ) + if response.status_code == 200: + return response.json()["access_token"] + else: + raise ValueError(f"Login failed: {response.status_code} - {response.text}") + + +async def main(): + password = "secure-password" + + # Log in as user 1 + user1_token = await login(email1, password) + user1_client = get_client( + url="http://localhost:2024", headers={"Authorization": f"Bearer {user1_token}"} + ) + + # Create a thread as user 1 + thread = await user1_client.threads.create() + print(f"✅ User 1 created thread: {thread['thread_id']}") + + # Try to access without a token + unauthenticated_client = get_client(url="http://localhost:2024") + try: + await unauthenticated_client.threads.create() + print("❌ Unauthenticated access should fail!") + except Exception as e: + print("✅ Unauthenticated access blocked:", e) + + # Try to access user 1's thread as user 2 + user2_token = await login(email2, password) + user2_client = get_client( + url="http://localhost:2024", headers={"Authorization": f"Bearer {user2_token}"} + ) + + try: + await user2_client.threads.get(thread["thread_id"]) + print("❌ User 2 shouldn't see User 1's thread!") + except Exception as e: + print("✅ User 2 blocked from User 1's thread:", e) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +Fetch the SUPABASE_ANON_KEY that you copied from the Supabase dashboard in step (1), then run the test. Make sure the server is running (if you have run `langgraph dev`): + +```bash +python test_oauth.py CHANGEME@example.com +``` + +> ➜ custom-auth SUPABASE_ANON_KEY=eyJh... python test_oauth.py CHANGEME@example.com +> ✅ User 1 created thread: d6af3754-95df-4176-aa10-dbd8dca40f1a +> ✅ Unauthenticated access blocked: Client error '403 Forbidden' for url 'http://localhost:2024/threads' +> ✅ User 2 blocked from User 1's thread: Client error '404 Not Found' for url 'http://localhost:2024/threads/d6af3754-95df-4176-aa10-dbd8dca40f1a' + +Perfect! Our authentication and authorization are working together: +1. Users must log in to access the bot +2. Each user can only see their own threads + +All our users are managed by the Supabase auth provider, so we don't need to implement any additional user management logic. + +## Congratulations! 🎉 + +You've successfully built a production-ready authentication system for your LangGraph application! Let's review what you've accomplished: + +1. Set up an authentication provider (Supabase in this case) +2. Added real user accounts with email/password authentication +3. Integrated JWT token validation into your LangGraph server +4. Implemented proper authorization to ensure users can only access their own data +5. Created a foundation that's ready to handle your next authentication challenge 🚀 + +This completes our authentication tutorial series. You now have the building blocks for a secure, production-ready LangGraph application. + +## What's Next? + +Now that you have production authentication, consider: + +1. Building a web UI with your preferred framework (see the [Custom Auth](https://github.com/langchain-ai/custom-auth) template for an example) +2. Learn more about the other aspects of authentication and authorization in the [conceptual guide on authentication](../../concepts/auth.md). +3. Customize your handlers and setup further after reading the [reference docs](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth). \ No newline at end of file diff --git a/docs/docs/tutorials/auth/resource_auth.md b/docs/docs/tutorials/auth/resource_auth.md new file mode 100644 index 000000000..16ac6a2d5 --- /dev/null +++ b/docs/docs/tutorials/auth/resource_auth.md @@ -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 From b3230cf6d19a1bc2221c66aaef40d95662710bbb Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:01:56 -0800 Subject: [PATCH 12/29] More guidance --- docs/docs/tutorials/auth/resource_auth.md | 37 +++++++++++++++-------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/docs/docs/tutorials/auth/resource_auth.md b/docs/docs/tutorials/auth/resource_auth.md index 16ac6a2d5..ec564aac7 100644 --- a/docs/docs/tutorials/auth/resource_auth.md +++ b/docs/docs/tutorials/auth/resource_auth.md @@ -167,14 +167,25 @@ Update `src/security/auth.py` to add handlers for specific resource types: ```python # Keep our previous handlers... +from langgraph_sdk import Auth + @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.""" + """Add owner when creating threads. + + This handler runs when creating new threads and does two things: + 1. Sets metadata on the thread being created to track ownership + 2. Returns a filter that ensures only the creator can access it + """ + # Add owner metadata to the thread being created + # This metadata is stored with the thread and persists metadata = value.setdefault("metadata", {}) metadata["owner"] = ctx.user.identity + + # Return filter to restrict access to just the creator return {"owner": ctx.user.identity} @auth.on.threads.read @@ -182,7 +193,12 @@ async def on_thread_read( ctx: Auth.types.AuthContext, value: Auth.types.on.threads.read.value, ): - """Only let users read their own threads.""" + """Only let users read their own threads. + + This handler runs on read operations. We don't need to set + metadata since the thread already exists - we just need to + return a filter to ensure users can only see their own threads. + """ return {"owner": ctx.user.identity} @auth.on.threads.create_run @@ -190,18 +206,13 @@ async def on_run_create( ctx: Auth.types.AuthContext, value: Auth.types.on.threads.create_run.value, ): - """Only let thread owners create runs.""" + """Only let thread owners create runs. + + This handler runs when creating runs on a thread. The filter + applies to the parent thread, not the run being created. + This ensures only thread owners can create runs on their threads. + """ 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: From 83e4e6c4c2350abc96849c33f154ec44f5138544 Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:15:38 -0800 Subject: [PATCH 13/29] Update docs/docs/tutorials/auth/add_auth_server.md Co-authored-by: Eugene Yurtsev --- docs/docs/tutorials/auth/add_auth_server.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/docs/tutorials/auth/add_auth_server.md b/docs/docs/tutorials/auth/add_auth_server.md index cfc5b86ca..f34a847fd 100644 --- a/docs/docs/tutorials/auth/add_auth_server.md +++ b/docs/docs/tutorials/auth/add_auth_server.md @@ -16,7 +16,6 @@ We'll keep the same [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgra !!! warning "Prerequisites" - - Complete the [Resource Authorization](resource_auth.md) tutorial - [Create a Supabase project](https://supabase.com/dashboard) - Have your project URL and service role key ready From 51c57cc81925d4038b81e2519963c1a71b1d298f Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 11:13:28 -0800 Subject: [PATCH 14/29] [SDK] Add studio user object --- libs/sdk-py/langgraph_sdk/auth/types.py | 51 +++++++++++++++++++++++++ libs/sdk-py/pyproject.toml | 2 +- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/libs/sdk-py/langgraph_sdk/auth/types.py b/libs/sdk-py/langgraph_sdk/auth/types.py index cc47651d0..2320586b9 100644 --- a/libs/sdk-py/langgraph_sdk/auth/types.py +++ b/libs/sdk-py/langgraph_sdk/auth/types.py @@ -176,6 +176,57 @@ def permissions(self) -> Sequence[str]: ... +class StudioUser: + """A user object that's populated from authenticated requests from the LangGraph studio. + + Note: Studio auth can be disabled in your `langgraph.json` config. + + ```json + { + "auth": { + "disable_studio_auth": true + } + } + ``` + + You can use `isinstance` checks in your authorization handlers (`@auth.on`) to control access specifically + for developers accessing the instance from the LangGraph Studio UI. + + ???+ example "Examples" + ```python + @auth.on + async def allow_developers(ctx: Auth.types.AuthContext, value: Any) -> None: + if isinstance(ctx.user, Auth.types.StudioUser): + return None + ... + return False + ``` + """ + + __slots__ = ("username", "_is_authenticated", "_permissions") + + def __init__(self, username: str, is_authenticated: bool = False) -> None: + self.username = username + self._is_authenticated = is_authenticated + self._permissions = ["authenticated"] if is_authenticated else [] + + @property + def is_authenticated(self) -> bool: + return self._is_authenticated + + @property + def display_name(self) -> str: + return self.username + + @property + def identity(self) -> str: + return self.username + + @property + def permissions(self) -> Sequence[str]: + return self._permissions + + Authenticator = Callable[ ..., Awaitable[ diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml index c73c8afa1..7ed7c49b1 100644 --- a/libs/sdk-py/pyproject.toml +++ b/libs/sdk-py/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph-sdk" -version = "0.1.47" +version = "0.1.48" description = "SDK for interacting with LangGraph API" authors = [] license = "MIT" From e20bd15580c3d1084fb4e33e04e4dbc1d896800c Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 18 Dec 2024 13:15:38 +0000 Subject: [PATCH 15/29] lib: Fix incorrect default for Command.update - this should not default to empty tuple, it should default to None --- libs/langgraph/langgraph/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py index 7dc3aeec2..6b90f8acd 100644 --- a/libs/langgraph/langgraph/types.py +++ b/libs/langgraph/langgraph/types.py @@ -271,7 +271,7 @@ class Command(Generic[N], ToolOutputMixin): """ graph: Optional[str] = None - update: Any = () + update: Optional[Any] = None resume: Optional[Union[Any, dict[str, Any]]] = None goto: Union[Send, Sequence[Union[Send, str]], str] = () From 7e8a68f0f09f2433f29d53c99c7d72addb2473fd Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 18 Dec 2024 13:28:22 +0000 Subject: [PATCH 16/29] Fix --- libs/langgraph/langgraph/types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/types.py b/libs/langgraph/langgraph/types.py index 6b90f8acd..53dc6bd57 100644 --- a/libs/langgraph/langgraph/types.py +++ b/libs/langgraph/langgraph/types.py @@ -292,8 +292,10 @@ def _update_as_tuples(self) -> Sequence[tuple[str, Any]]: for t in self.update ): return self.update - else: + elif self.update is not None: return [("__root__", self.update)] + else: + return [] PARENT: ClassVar[Literal["__parent__"]] = "__parent__" From ef88b805a73079f8484eb3278dbf1cab50e94365 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Wed, 18 Dec 2024 13:45:32 +0000 Subject: [PATCH 17/29] 0.2.60 --- libs/langgraph/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 35c47866a..4197552bd 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "langgraph" -version = "0.2.59" +version = "0.2.60" description = "Building stateful, multi-actor applications with LLMs" authors = [] license = "MIT" From dabd75f1f97409b5eeb0e12a9eda6abdac83aa8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:04:05 -0500 Subject: [PATCH 18/29] build(deps-dev): bump tornado from 6.4.1 to 6.4.2 (#2519) Bumps [tornado](https://github.com/tornadoweb/tornado) from 6.4.1 to 6.4.2.
Changelog

Sourced from tornado's changelog.

Release notes

.. toctree:: :maxdepth: 2

releases/v6.4.2 releases/v6.4.1 releases/v6.4.0 releases/v6.3.3 releases/v6.3.2 releases/v6.3.1 releases/v6.3.0 releases/v6.2.0 releases/v6.1.0 releases/v6.0.4 releases/v6.0.3 releases/v6.0.2 releases/v6.0.1 releases/v6.0.0 releases/v5.1.1 releases/v5.1.0 releases/v5.0.2 releases/v5.0.1 releases/v5.0.0 releases/v4.5.3 releases/v4.5.2 releases/v4.5.1 releases/v4.5.0 releases/v4.4.3 releases/v4.4.2 releases/v4.4.1 releases/v4.4.0 releases/v4.3.0 releases/v4.2.1 releases/v4.2.0 releases/v4.1.0 releases/v4.0.2 releases/v4.0.1 releases/v4.0.0 releases/v3.2.2 releases/v3.2.1 releases/v3.2.0 releases/v3.1.1 releases/v3.1.0 releases/v3.0.2 releases/v3.0.1 releases/v3.0.0 releases/v2.4.1 releases/v2.4.0

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tornado&package-manager=pip&previous-version=6.4.1&new-version=6.4.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/langchain-ai/langgraph/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vadym Barda --- poetry.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9b0fe29f8..94f2b3db3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6748,22 +6748,22 @@ files = [ [[package]] name = "tornado" -version = "6.4.1" +version = "6.4.2" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." optional = false python-versions = ">=3.8" files = [ - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, - {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, - {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, - {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, - {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, - {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, - {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, + {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"}, + {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"}, + {file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"}, + {file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"}, + {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, ] [[package]] From 532bc71c118378e17b62af4d4abc6d86e9ada7a6 Mon Sep 17 00:00:00 2001 From: Bagatur <22008038+baskaryan@users.noreply.github.com> Date: Wed, 18 Dec 2024 09:19:04 -0800 Subject: [PATCH 19/29] langgraph[patch]: format messages in state (#2199) Add `format` flag to `add_messages` which allows you to specify if the contents of messages in state should be formatted in a particular way. PR only adds support for OpenAI style contents. Helpful if you're using different models at different nodes and want a unified messages format to interact with when you manually update messages. --- libs/langgraph/langgraph/graph/message.py | 126 +++++++++++++++++++- libs/langgraph/langgraph/graph/state.py | 8 +- libs/langgraph/poetry.lock | 12 +- libs/langgraph/tests/test_messages_state.py | 110 +++++++++++++++++ 4 files changed, 246 insertions(+), 10 deletions(-) diff --git a/libs/langgraph/langgraph/graph/message.py b/libs/langgraph/langgraph/graph/message.py index 6575bd10c..e63ebee83 100644 --- a/libs/langgraph/langgraph/graph/message.py +++ b/libs/langgraph/langgraph/graph/message.py @@ -1,8 +1,21 @@ import uuid -from typing import Annotated, TypedDict, Union, cast +import warnings +from functools import partial +from typing import ( + Annotated, + Any, + Callable, + Literal, + Optional, + Sequence, + TypedDict, + Union, + cast, +) from langchain_core.messages import ( AnyMessage, + BaseMessage, BaseMessageChunk, MessageLikeRepresentation, RemoveMessage, @@ -15,7 +28,32 @@ Messages = Union[list[MessageLikeRepresentation], MessageLikeRepresentation] -def add_messages(left: Messages, right: Messages) -> Messages: +def _add_messages_wrapper(func: Callable) -> Callable[[Messages, Messages], Messages]: + def _add_messages( + left: Optional[Messages] = None, right: Optional[Messages] = None, **kwargs: Any + ) -> Union[Messages, Callable[[Messages, Messages], Messages]]: + if left is not None and right is not None: + return func(left, right, **kwargs) + elif left is not None or right is not None: + msg = ( + f"Must specify non-null arguments for both 'left' and 'right'. Only " + f"received: '{'left' if left else 'right'}'." + ) + raise ValueError(msg) + else: + return partial(func, **kwargs) + + _add_messages.__doc__ = func.__doc__ + return cast(Callable[[Messages, Messages], Messages], _add_messages) + + +@_add_messages_wrapper +def add_messages( + left: Messages, + right: Messages, + *, + format: Optional[Literal["langchain-openai"]] = None, +) -> Messages: """Merges two lists of messages, updating existing messages by ID. By default, this ensures the state is "append-only", unless the @@ -25,6 +63,14 @@ def add_messages(left: Messages, right: Messages) -> Messages: left: The base list of messages. right: The list of messages (or single message) to merge into the base list. + format: The format to return messages in. If None then messages will be + returned as is. If 'langchain-openai' then messages will be returned as + BaseMessage objects with their contents formatted to match OpenAI message + format, meaning contents can be string, 'text' blocks, or 'image_url' blocks + and tool responses are returned as their own ToolMessages. + + **REQUIREMENT**: Must have ``langchain-core>=0.3.11`` installed to use this + feature. Returns: A new list of messages with the messages from `right` merged into `left`. @@ -58,8 +104,59 @@ def add_messages(left: Messages, right: Messages) -> Messages: >>> graph = builder.compile() >>> graph.invoke({}) {'messages': [AIMessage(content='Hello', id=...)]} + + >>> from typing import Annotated + >>> from typing_extensions import TypedDict + >>> from langgraph.graph import StateGraph, add_messages + >>> + >>> class State(TypedDict): + ... messages: Annotated[list, add_messages(format='langchain-openai')] + ... + >>> def chatbot_node(state: State) -> list: + ... return {"messages": [ + ... { + ... "role": "user", + ... "content": [ + ... { + ... "type": "text", + ... "text": "Here's an image:", + ... "cache_control": {"type": "ephemeral"}, + ... }, + ... { + ... "type": "image", + ... "source": { + ... "type": "base64", + ... "media_type": "image/jpeg", + ... "data": "1234", + ... }, + ... }, + ... ] + ... }, + ... ]} + >>> builder = StateGraph(State) + >>> builder.add_node("chatbot", chatbot_node) + >>> builder.set_entry_point("chatbot") + >>> builder.set_finish_point("chatbot") + >>> graph = builder.compile() + >>> graph.invoke({"messages": []}) + { + 'messages': [ + HumanMessage( + content=[ + {"type": "text", "text": "Here's an image:"}, + { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,1234"}, + }, + ], + ), + ] + } ``` + ..versionchanged:: 0.2.61 + + Support for 'format="langchain-openai"' flag added. """ # coerce to list if not isinstance(left, list): @@ -100,6 +197,15 @@ def add_messages(left: Messages, right: Messages) -> Messages: merged.append(m) merged = [m for m in merged if m.id not in ids_to_remove] + + if format == "langchain-openai": + merged = _format_messages(merged) + elif format: + msg = f"Unrecognized {format=}. Expected one of 'langchain-openai', None." + raise ValueError(msg) + else: + pass + return merged @@ -156,3 +262,19 @@ def __init__(self) -> None: class MessagesState(TypedDict): messages: Annotated[list[AnyMessage], add_messages] + + +def _format_messages(messages: Sequence[BaseMessage]) -> list[BaseMessage]: + try: + from langchain_core.messages import convert_to_openai_messages + except ImportError: + msg = ( + "Must have langchain-core>=0.3.11 installed to use automatic message " + "formatting (format='langchain-openai'). Please update your langchain-core " + "version or remove the 'format' flag. Returning un-formatted " + "messages." + ) + warnings.warn(msg) + return list(messages) + else: + return convert_to_messages(convert_to_openai_messages(messages)) diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index 7a5614f91..e412db2d5 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -961,8 +961,12 @@ def _is_field_binop(typ: Type[Any]) -> Optional[BinaryOperatorAggregate]: if len(meta) >= 1 and callable(meta[-1]): sig = signature(meta[-1]) params = list(sig.parameters.values()) - if len(params) == 2 and all( - p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD) for p in params + if ( + sum( + p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD) + for p in params + ) + == 2 ): return BinaryOperatorAggregate(typ, meta[-1]) else: diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index bdb2a4da6..deace7b65 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -1325,18 +1325,18 @@ files = [ [[package]] name = "langchain-core" -version = "0.3.23" +version = "0.3.25" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "langchain_core-0.3.23-py3-none-any.whl", hash = "sha256:550c0b996990830fa6515a71a1192a8a0343367999afc36d4ede14222941e420"}, - {file = "langchain_core-0.3.23.tar.gz", hash = "sha256:f9e175e3b82063cc3b160c2ca2b155832e1c6f915312e1204828f97d4aabf6e1"}, + {file = "langchain_core-0.3.25-py3-none-any.whl", hash = "sha256:e10581c6c74ba16bdc6fdf16b00cced2aa447cc4024ed19746a1232918edde38"}, + {file = "langchain_core-0.3.25.tar.gz", hash = "sha256:fdb8df41e5cdd928c0c2551ebbde1cea770ee3c64598395367ad77ddf9acbae7"}, ] [package.dependencies] jsonpatch = ">=1.33,<2.0" -langsmith = ">=0.1.125,<0.2.0" +langsmith = ">=0.1.125,<0.3" packaging = ">=23.2,<25" pydantic = [ {version = ">=2.5.2,<3.0.0", markers = "python_full_version < \"3.12.4\""}, @@ -1348,7 +1348,7 @@ typing-extensions = ">=4.7" [[package]] name = "langgraph-checkpoint" -version = "2.0.8" +version = "2.0.9" description = "Library with base interfaces for LangGraph checkpoint savers." optional = false python-versions = "^3.9.0,<4.0" @@ -1418,7 +1418,7 @@ url = "../checkpoint-sqlite" [[package]] name = "langgraph-sdk" -version = "0.1.43" +version = "0.1.47" description = "SDK for interacting with LangGraph API" optional = false python-versions = "^3.9.0,<4.0" diff --git a/libs/langgraph/tests/test_messages_state.py b/libs/langgraph/tests/test_messages_state.py index ff8d064d6..787774baf 100644 --- a/libs/langgraph/tests/test_messages_state.py +++ b/libs/langgraph/tests/test_messages_state.py @@ -1,6 +1,7 @@ from typing import Annotated from uuid import UUID +import langchain_core import pytest from langchain_core.messages import ( AIMessage, @@ -8,9 +9,11 @@ HumanMessage, RemoveMessage, SystemMessage, + ToolMessage, ) from pydantic import BaseModel from pydantic.v1 import BaseModel as BaseModelV1 +from typing_extensions import TypedDict from langgraph.graph import add_messages from langgraph.graph.message import MessagesState @@ -18,6 +21,8 @@ from tests.conftest import IS_LANGCHAIN_CORE_030_OR_GREATER from tests.messages import _AnyIdHumanMessage +_, CORE_MINOR, CORE_PATCH = (int(v) for v in langchain_core.__version__.split(".")) + def test_add_single_message(): left = [HumanMessage(content="Hello", id="1")] @@ -178,3 +183,108 @@ def foo(state): _AnyIdHumanMessage(content="foo"), ] } + + +@pytest.mark.skipif( + condition=not ((CORE_MINOR == 3 and CORE_PATCH >= 11) or CORE_MINOR > 3), + reason="Requires langchain_core>=0.3.11.", +) +def test_messages_state_format_openai(): + class State(TypedDict): + messages: Annotated[list[AnyMessage], add_messages(format="langchain-openai")] + + def foo(state): + messages = [ + HumanMessage( + content=[ + { + "type": "text", + "text": "Here's an image:", + "cache_control": {"type": "ephemeral"}, + }, + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "1234", + }, + }, + ] + ), + AIMessage( + content=[ + { + "type": "tool_use", + "name": "foo", + "input": {"bar": "baz"}, + "id": "1", + } + ] + ), + HumanMessage( + content=[ + { + "type": "tool_result", + "tool_use_id": "1", + "is_error": False, + "content": [ + { + "type": "image", + "source": { + "type": "base64", + "media_type": "image/jpeg", + "data": "1234", + }, + }, + ], + } + ] + ), + ] + return {"messages": messages} + + expected = [ + HumanMessage(content="meow"), + HumanMessage( + content=[ + {"type": "text", "text": "Here's an image:"}, + { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,1234"}, + }, + ], + ), + AIMessage( + content="", + tool_calls=[ + { + "name": "foo", + "type": "tool_calls", + "args": {"bar": "baz"}, + "id": "1", + } + ], + ), + ToolMessage( + content=[ + { + "type": "image_url", + "image_url": {"url": "data:image/jpeg;base64,1234"}, + } + ], + tool_call_id="1", + ), + ] + + graph = StateGraph(State) + graph.add_edge(START, "foo") + graph.add_edge("foo", END) + graph.add_node(foo) + + app = graph.compile() + + result = app.invoke({"messages": [("user", "meow")]}) + for m in result["messages"]: + m.id = None + assert result == {"messages": expected} From 6b45a281c14104279e71ccadf92e1bbdb10ff0e2 Mon Sep 17 00:00:00 2001 From: Yang Yang Date: Thu, 19 Dec 2024 01:46:33 +0800 Subject: [PATCH 20/29] fix example in docs of state_schema in create_react_agent (#2109) because in ```python def call_model( state: AgentState, config: RunnableConfig, ) ... if ( ( "remaining_steps" not in state and state["is_last_step"] and has_tool_calls ) ``` https://github.com/langchain-ai/langgraph/blob/c0b56bf60d84ed435609c35b0691cd0305ceae78/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py#L543 the AgentState requires is_last_step to have a default value, like `False`, and `IsLastStep` can satisfy it. --------- Co-authored-by: Vadym Barda --- libs/langgraph/langgraph/prebuilt/chat_agent_executor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py index 4c8699360..b5207d167 100644 --- a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py +++ b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py @@ -382,6 +382,8 @@ class Agent,Tools otherClass ```pycon >>> from typing import TypedDict + >>> + >>> from langgraph.managed import IsLastStep >>> prompt = ChatPromptTemplate.from_messages( ... [ ... ("system", "Today is {today}"), @@ -392,7 +394,7 @@ class Agent,Tools otherClass >>> class CustomState(TypedDict): ... today: str ... messages: Annotated[list[BaseMessage], add_messages] - ... is_last_step: str + ... is_last_step: IsLastStep >>> >>> graph = create_react_agent(model, tools, state_schema=CustomState, state_modifier=prompt) >>> inputs = {"messages": [("user", "What's today's date? And what's the weather in SF?")], "today": "July 16, 2004"} From d9cc227e750b34da4f51f551db4c9bec465ae847 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:03:16 -0800 Subject: [PATCH 21/29] Unpin install command in doc --- docs/docs/how-tos/local-studio.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/how-tos/local-studio.md b/docs/docs/how-tos/local-studio.md index 8e4e29f89..c86811191 100644 --- a/docs/docs/how-tos/local-studio.md +++ b/docs/docs/how-tos/local-studio.md @@ -23,7 +23,7 @@ You will need to install [`langgraph-cli`](../cloud/reference/cli.md#langgraph-c You will need to make sure to install the `inmem` extras. ```shell -pip install "langgraph-cli[inmem]==0.1.55" +pip install -U "langgraph-cli[inmem]" ``` ## Run the development server From 1e07a9ac97b7464fea696d4376e20b8093fbf566 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 10:04:57 -0800 Subject: [PATCH 22/29] Add admonition --- docs/docs/how-tos/local-studio.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/docs/how-tos/local-studio.md b/docs/docs/how-tos/local-studio.md index c86811191..270bd80c0 100644 --- a/docs/docs/how-tos/local-studio.md +++ b/docs/docs/how-tos/local-studio.md @@ -22,6 +22,12 @@ See [this guide](../concepts/application_structure.md) for information on how to You will need to install [`langgraph-cli`](../cloud/reference/cli.md#langgraph-cli) (version `0.1.55` or higher). You will need to make sure to install the `inmem` extras. +???+ note "Minimum version" + + The minimum version to use the `inmem` extra with `langgraph-cli` is `0.1.55`. + Python 3.11 or higher is required. + + ```shell pip install -U "langgraph-cli[inmem]" ``` From 7f0bfdc139b0ff2cfc106b36997421b0b0fb667d Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:15:54 -0800 Subject: [PATCH 23/29] Feedback --- docs/docs/cloud/reference/cli.md | 3 +- .../cloud/reference/sdk/python_sdk_ref.md | 14 +- docs/docs/concepts/auth.md | 214 ++++++++++++++---- docs/docs/how-tos/auth/custom_auth.md | 6 +- docs/docs/tutorials/auth/add_auth_server.md | 167 ++++++-------- docs/docs/tutorials/auth/getting_started.md | 24 +- docs/docs/tutorials/auth/resource_auth.md | 8 +- libs/sdk-py/langgraph_sdk/auth/exceptions.py | 15 -- libs/sdk-py/langgraph_sdk/auth/types.py | 36 +-- 9 files changed, 277 insertions(+), 210 deletions(-) diff --git a/docs/docs/cloud/reference/cli.md b/docs/docs/cloud/reference/cli.md index 004103533..72599e7fc 100644 --- a/docs/docs/cloud/reference/cli.md +++ b/docs/docs/cloud/reference/cli.md @@ -287,4 +287,5 @@ RUN set -ex && \ RUN PIP_CONFIG_FILE=/pipconfig.txt PYTHONDONTWRITEBYTECODE=1 pip install --no-cache-dir -c /api/constraints.txt -e /deps/* -ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}' \ No newline at end of file +ENV LANGSERVE_GRAPHS='{"agent": "/deps/__outer_graphs/src/agent.py:graph", "storm": "/deps/__outer_graphs/src/storm.py:graph"}' +``` \ No newline at end of file diff --git a/docs/docs/cloud/reference/sdk/python_sdk_ref.md b/docs/docs/cloud/reference/sdk/python_sdk_ref.md index 3fde9e9cb..7ac7aa165 100644 --- a/docs/docs/cloud/reference/sdk/python_sdk_ref.md +++ b/docs/docs/cloud/reference/sdk/python_sdk_ref.md @@ -10,19 +10,7 @@ ::: langgraph_sdk.auth handler: python -::: langgraph_sdk.auth.types.Authenticator - handler: python - -::: langgraph_sdk.auth.types.Handler - handler: python - -::: langgraph_sdk.auth.types.HandlerResult - handler: python - -::: langgraph_sdk.auth.types.FilterType - handler: python - -::: langgraph_sdk.auth.types.AuthContext +::: langgraph_sdk.auth.types handler: python ::: langgraph_sdk.auth.exceptions diff --git a/docs/docs/concepts/auth.md b/docs/docs/concepts/auth.md index 54b42cdd2..c14135c93 100644 --- a/docs/docs/concepts/auth.md +++ b/docs/docs/concepts/auth.md @@ -1,9 +1,9 @@ # Authentication & Access Control -LangGraph Platform provides a flexible authentication and authorization system that can integrate with most authentication schemes. This guide explains the core concepts and how they work together. +LangGraph Platform provides a flexible authentication and authorization system that can integrate with most authentication schemes. !!! note "Python only" - + We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon. ## Core Concepts @@ -22,12 +22,14 @@ In LangGraph Platform, authentication is handled by your [`@auth.authenticate`]( A typical authentication setup involves three main components: 1. **Authentication Provider** (Identity Provider/IdP) + - A dedicated service that manages user identities and credentials - Examples: Auth0, Supabase Auth, Okta, or your own auth server - Handles user registration, login, password resets, etc. - Issues tokens (JWT, session tokens, etc.) after successful authentication 2. **LangGraph Backend** (Resource Server) + - Your LangGraph application that contains business logic and protected resources - Validates tokens with the auth provider - Enforces access control based on user identity and permissions @@ -46,7 +48,7 @@ sequenceDiagram participant Client as Client App participant Auth as Auth Provider participant LG as LangGraph Backend - + Client->>Auth: 1. Login (username/password) Auth-->>Client: 2. Return token Client->>LG: 3. Request with token @@ -104,57 +106,132 @@ The returned user information is available: After authentication, LangGraph calls your `@auth.on` handlers to control access to specific resources (e.g., threads, assistants, crons). These handlers can: 1. Add metadata to be saved during resource creation by mutating the `value["metadata"]` dictionary directly. -2. Filter resources by metadata during search/list or read operations by returning a filter dictionary. +2. Filter resources by metadata during search/list or read operations by returning a [filter dictionary](#filter-operations). 3. Raise an HTTP exception if access is denied. -If you want to just implement simple user-scoped access control, you can use a single `@auth.on` handler for all resources and actions. +If you want to just implement simple user-scoped access control, you can use a single `@auth.on` handler for all resources and actions. If you want to have different control depending on the resource and action, you can use [resource-specific handlers](#resource-specific-handlers). See the [Supported Resources](#supported-resources) section for a full list of the resources that support access control. ```python @auth.on -async def add_owner(ctx: Auth.types.AuthContext, value: dict): - """Add owner to resource metadata and filter by owner.""" +async def add_owner( + ctx: Auth.types.AuthContext, + value: dict # The payload being sent to this access method +) -> dict: # Returns a filter dict that restricts access to resources + """Authorize all access to threads, runs, crons, and assistants. + + This handler does two things: + - Adds a value to resource metadata (to persist with the resource so it can be filtered later) + - Returns a filter (to restrict access to existing resources) + + Args: + ctx: Authentication context containing user info, permissions, the path, and + value: The request payload sent to the endpoint. For creation + operations, this contains the resource parameters. For read + operations, this contains the resource being accessed. + + Returns: + A filter dictionary that LangGraph uses to restrict access to resources. + See [Filter Operations](#filter-operations) for supported operators. + """ + # Create filter to restrict access to just this user's resources filters = {"owner": ctx.user.identity} + + # Get or create the metadata dictionary in the payload + # This is where we store persistent info about the resource metadata = value.setdefault("metadata", {}) + + # Add owner to metadata - if this is a create or update operation, + # this information will be saved with the resource + # So we can filter by it later in read operations metadata.update(filters) + + # Return filters to restrict access + # These filters are applied to ALL operations (create, read, update, search, etc.) + # to ensure users can only access their own resources return filters ``` -### Resource-Specific Handlers +### Resource-Specific Handlers {#resource-specific-handlers} + +You can register handlers for specific resources and actions by chaining the resource and action names together with the `@auth.on` decorator. +When a request is made, the most specific handler that matches that resource and action is called. Below is an example of how to register handlers for specific resources and actions. For the following setup: + +1. Authenticated users are able to create threads, read thread, create runs on threads +2. Only users with the "assistants:create" permission are allowed to create new assistants +3. All other endpoints (e.g., e.g., delete assistant, crons, store) are disabled for all users. + +!!! tip "Supported Handlers" -You can register handlers for specific resources and actions using the `@auth.on` decorator. -When a request is made, the most specific handler that matches that resource and action is called. + For a full list of supported resources and actions, see the [Supported Resources](#supported-resources) section below. ```python # Generic / global handler catches calls that aren't handled by more specific handlers @auth.on -async def reject_unhandled_requests(ctx: Auth.types.AuthContext, value: Any) -> None: +async def reject_unhandled_requests(ctx: Auth.types.AuthContext, value: Any) -> False: print(f"Request to {ctx.path} by {ctx.user.identity}") - return False + raise Auth.exceptions.HTTPException( + status_code=403, + detail="Forbidden" + ) + +# Matches the "thread" resource and all actions - create, read, update, delete, search +# Since this is **more specific** than the generic @auth.on handler, it will take precedence +# over the generic handler for all actions on the "threads" resource +@auth.on.threads +async def on_thread_create( + ctx: Auth.types.AuthContext, + value: Auth.types.threads.create.value +): + if "write" not in ctx.permissions: + raise Auth.exceptions.HTTPException( + status_code=403, + detail="User lacks the required permissions." + ) + # Setting metadata on the thread being created + # will ensure that the resource contains an "owner" field + # Then any time a user tries to access this thread or runs within the thread, + # we can filter by owner + metadata = value.setdefault("metadata", {}) + metadata["owner"] = ctx.user.identity + return {"owner": ctx.user.identity} -# Thread creation +# Thread creation. This will match only on thread create actions +# Since this is **more specific** than both the generic @auth.on handler and the @auth.on.threads handler, +# it will take precedence for any "create" actions on the "threads" resources @auth.on.threads.create async def on_thread_create( ctx: Auth.types.AuthContext, value: Auth.types.threads.create.value ): + # Setting metadata on the thread being created + # will ensure that the resource contains an "owner" field + # Then any time a user tries to access this thread or runs within the thread, + # we can filter by owner metadata = value.setdefault("metadata", {}) metadata["owner"] = ctx.user.identity return {"owner": ctx.user.identity} -# Thread retrieval +# Reading a thread. Since this is also more specific than the generic @auth.on handler, and the @auth.on.threads handler, +# it will take precedence for any "read" actions on the "threads" resource @auth.on.threads.read async def on_thread_read( ctx: Auth.types.AuthContext, value: Auth.types.threads.read.value ): + # Since we are reading (and not creating) a thread, + # we don't need to set metadata. We just need to + # return a filter to ensure users can only see their own threads return {"owner": ctx.user.identity} # Run creation, streaming, updates, etc. +# This takes precendence over the generic @auth.on handler and the @auth.on.threads handler @auth.on.threads.create_run async def on_run_create( ctx: Auth.types.AuthContext, value: Auth.types.threads.create_run.value ): + metadata = value.setdefault("metadata", {}) + metadata["owner"] = ctx.user.identity # Inherit thread's access control return {"owner": ctx.user.identity} @@ -164,23 +241,31 @@ async def on_assistant_create( ctx: Auth.types.AuthContext, value: Auth.types.assistants.create.value ): - if "admin" not in ctx.user.get("role", []): + if "assistants:create" not in ctx.permissions: raise Auth.exceptions.HTTPException( status_code=403, - detail="Only admins can create assistants" + detail="User lacks the required permissions." ) ``` -Using the setup above, a request to create a `thread` would match the `on_thread_create` handler, since it is the most specific handler for that resource and action. A request to create a `cron`, on the other hand, would match the global handler, since no more specific handler is registered for that resource and action. +Notice that we are mixing global and resource-specific handlers in the above example. Since each request is handled by the most specific handler, a request to create a `thread` would match the `on_thread_create` handler but NOT the `reject_unhandled_requests` handler. A request to `update` a thread, however would be handled by the global handler, since we don't have a more specific handler for that resource and action. Requests to create, update, + +### Filter Operations {#filter-operations} -### Filter Operations +Authorization handlers can return `None`, a boolean, or a filter dictionary. +- `None` and `True` mean "authorize access to all underling resources" +- `False` means "deny access to all underling resources (raises a 403 exception)" +- A metadata filter dictionary will restrict access to resources -Authorization handlers can return a filter dictionary to filter resources during all operations (both reads and writes). The filter dictionary supports two additional operators: +A filter dictionary is a dictionary with keys that match the resource metadata. It supports three operators: -- `$eq`: Exact match (e.g., `{"owner": {"$eq": user_id}}`) - this is equivalent to `{"owner": user_id}` -- `$contains`: List membership (e.g., `{"allowed_users": {"$contains": user_id}}`) +- The default value is a shorthand for exact match, or "$eq", below. For example, `{"owner": user_id}` will include only resources with metadata containing `{"owner": user_id}` +- `$eq`: Exact match (e.g., `{"owner": {"$eq": user_id}}`) - this is equivalent to the shorthand above, `{"owner": user_id}` +- `$contains`: List membership (e.g., `{"allowed_users": {"$contains": user_id}}`) The value here must be an element of the list. The metadata in the stored resource must be a list/container type. -A dictionary with multiple keys is converted to a logical `AND` filter. For example, `{"owner": user_id, "org_id": org_id}` is converted to `{"$and": [{"owner": user_id}, {"org_id": org_id}]}` +A dictionary with multiple keys is treated using a logical `AND` filter. For example, `{"owner": org_id, "allowed_users": {"$contains": user_id}}` will only match resources with metadata whose "owner" is `org_id` and whose "allowed_users" list contains `user_id`. + +See the reference [here](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.FilterType) for more information. ## Common Access Patterns @@ -188,6 +273,8 @@ Here are some typical authorization patterns: ### Single-Owner Resources +This common pattern lets you scope all threads, assistants, crons, and runs to a single user. It's useful for common single-user use cases like regular chatbot-style apps. + ```python @auth.on async def owner_only(ctx: Auth.types.AuthContext, value: dict): @@ -198,6 +285,8 @@ async def owner_only(ctx: Auth.types.AuthContext, value: dict): ### Permission-based Access +This pattern lets you control access based on **permissions**. It's useful if you want certain roles to have broader or more restricted access to resources. + ```python # In your auth handler: @auth.authenticate @@ -238,34 +327,67 @@ async def rbac_create(ctx: Auth.types.AuthContext, value: dict): LangGraph provides authorization handlers for the following resource types: -### Threads -- `@auth.on.threads.create` - Thread creation -- `@auth.on.threads.read` - Thread retrieval -- `@auth.on.threads.update` - Thread updates -- `@auth.on.threads.delete` - Thread deletion -- `@auth.on.threads.search` - Listing threads - -**Runs:** are scoped to their parent thread for access control. This means permissions are typically inherited from the thread, reflecting the conversational nature of the data model. - -- `@auth.on.threads.create_run` - Creating or updating a run +## Supported Resources -All other run operations (reading, listing) are controlled by the thread's handlers, since runs are always accessed in the context of their thread. +LangGraph provides three levels of authorization handlers, from most general to most specific: -### Assistants -- `@auth.on.assistants.create` - Assistant creation -- `@auth.on.assistants.read` - Assistant retrieval -- `@auth.on.assistants.update` - Assistant updates -- `@auth.on.assistants.delete` - Assistant deletion -- `@auth.on.assistants.search` - Listing assistants +1. **Global Handler** (`@auth.on`): Matches all resources and actions +2. **Resource Handler** (e.g., `@auth.on.threads`, `@auth.on.assistants`, `@auth.on.crons`): Matches all actions for a specific resource +3. **Action Handler** (e.g., `@auth.on.threads.create`, `@auth.on.threads.read`): Matches a specific action on a specific resource -### Crons -- `@auth.on.crons.create` - Cron job creation -- `@auth.on.crons.read` - Cron job retrieval -- `@auth.on.crons.update` - Cron job updates -- `@auth.on.crons.delete` - Cron job deletion -- `@auth.on.crons.search` - Listing cron jobs +The most specific matching handler will be used. For example, `@auth.on.threads.create` takes precedence over `@auth.on.threads` for thread creation. +If a more specific handler is registered, the more general handler will not be called for that resource and action. -You can also use the global `@auth.on` handler to implement a single access control policy across all resources and actions, or resource level `@auth.on.threads`, etc. handlers to implement control over all actions of a single resource. +???+ tip "Type Safety" + Each handler has type hints available for its `value` parameter at `Auth.types.on...value`. For example: + ```python + @auth.on.threads.create + async def on_thread_create( + ctx: Auth.types.AuthContext, + value: Auth.types.on.threads.create.value # Specific type for thread creation + ): + ... + + @auth.on.threads + async def on_threads( + ctx: Auth.types.AuthContext, + value: Auth.types.on.threads.value # Union type of all thread actions + ): + ... + + @auth.on + async def on_all( + ctx: Auth.types.AuthContext, + value: dict # Union type of all possible actions + ): + ... + ``` + More specific handlers provide better type hints since they handle fewer action types. + +Here are all the supported action handlers: + +| Resource | Handler | Description | +|----------|---------|-------------| +| **Threads** | `@auth.on.threads.create` | Thread creation | +| | `@auth.on.threads.read` | Thread retrieval | +| | `@auth.on.threads.update` | Thread updates | +| | `@auth.on.threads.delete` | Thread deletion | +| | `@auth.on.threads.search` | Listing threads | +| | `@auth.on.threads.create_run` | Creating or updating a run | +| **Assistants** | `@auth.on.assistants.create` | Assistant creation | +| | `@auth.on.assistants.read` | Assistant retrieval | +| | `@auth.on.assistants.update` | Assistant updates | +| | `@auth.on.assistants.delete` | Assistant deletion | +| | `@auth.on.assistants.search` | Listing assistants | +| **Crons** | `@auth.on.crons.create` | Cron job creation | +| | `@auth.on.crons.read` | Cron job retrieval | +| | `@auth.on.crons.update` | Cron job updates | +| | `@auth.on.crons.delete` | Cron job deletion | +| | `@auth.on.crons.search` | Listing cron jobs | + +???+ note "About Runs" + Runs are scoped to their parent thread for access control. This means permissions are typically inherited from the thread, reflecting the conversational nature of the data model. All run operations (reading, listing) except creation are controlled by the thread's handlers. + There is a specific `create_run` handler for creating new runs because it had more arguments that you can view in the handler. ## Default Security Models diff --git a/docs/docs/how-tos/auth/custom_auth.md b/docs/docs/how-tos/auth/custom_auth.md index 8f9fd77cf..9ac43d579 100644 --- a/docs/docs/how-tos/auth/custom_auth.md +++ b/docs/docs/how-tos/auth/custom_auth.md @@ -1,7 +1,5 @@ # How to add custom authentication -This guide shows how to add custom authentication to your LangGraph Platform application. This guide applies to both LangGraph Cloud, BYOC, and self-hosted deployments. It does not apply to isolated usage of the LangGraph open source library in your own custom server. - !!! tip "Prerequisites" This guide assumes familiarity with the following concepts: @@ -11,10 +9,12 @@ This guide shows how to add custom authentication to your LangGraph Platform app For a more guided walkthrough, see [**setting up custom authentication**](../../tutorials/auth/getting_started.md) tutorial. -!!! note "Python only" +???+ note "Python only" We currently only support custom authentication and authorization in Python deployments with `langgraph-api>=0.0.11`. Support for LangGraph.JS will be added soon. +This guide shows how to add custom authentication to your LangGraph Platform application. This guide applies to both LangGraph Cloud, BYOC, and self-hosted deployments. It does not apply to isolated usage of the LangGraph open source library in your own custom server. + ## 1. Implement authentication Create `auth.py` file, with a basic JWT authentication handler: diff --git a/docs/docs/tutorials/auth/add_auth_server.md b/docs/docs/tutorials/auth/add_auth_server.md index f34a847fd..fe99803af 100644 --- a/docs/docs/tutorials/auth/add_auth_server.md +++ b/docs/docs/tutorials/auth/add_auth_server.md @@ -1,6 +1,12 @@ -# Connecting an Authentication Provider +# Connecting an Authentication Provider (Part 3/3) -In the previous tutorial, we added [resource authorization](../../concepts/auth.md#resource-authorization) to give users private conversations. However, we were still using hard-coded tokens for authentication, which is not secure. Now we'll replace those tokens with real user accounts using [OAuth2](../../concepts/auth.md#oauth2-authentication). +!!! note "This is part 3 of our authentication series:" + + 1. [Basic Authentication](getting_started.md) - Control who can access your bot + 2. [Resource Authorization](resource_auth.md) - Let users have private conversations + 3. Production Auth (you are here) - Add real user accounts and validate using OAuth2 + +In the [Making Conversations Private](resource_auth.md) tutorial, we added [resource authorization](../../concepts/auth.md#resource-authorization) to give users private conversations. However, we were still using hard-coded tokens for authentication, which is not secure. Now we'll replace those tokens with real user accounts using [OAuth2](../../concepts/auth.md#oauth2-authentication). We'll keep the same [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object and [resource-level access control](../../concepts/auth.md#resource-level-access-control), but upgrade our authentication to use Supabase as our identity provider. While we use Supabase in this tutorial, the concepts apply to any OAuth2 provider. You'll learn how to: @@ -8,16 +14,9 @@ We'll keep the same [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgra 2. Integrate with OAuth2 providers for secure user authentication 3. Handle user sessions and metadata while maintaining our existing authorization logic -!!! note "This is part 3 of our authentication series:" - - 1. [Basic Authentication](getting_started.md) - Control who can access your bot - 2. [Resource Authorization](resource_auth.md) - Let users have private conversations - 3. Production Auth (you are here) - Add real user accounts and validate using OAuth2 - -!!! warning "Prerequisites" +## Requirements - - [Create a Supabase project](https://supabase.com/dashboard) - - Have your project URL and service role key ready +You will need to set up a Supabase project to use its authentication server for this tutorial. You can do so [here](https://supabase.com/dashboard). ## Background @@ -49,7 +48,7 @@ sequenceDiagram In the following example, we'll use Supabase as our auth server. The LangGraph application will provide the backend for your app, and we will write test code for the client app. Let's get started! -## Setting Up Authentication Provider +## Setting Up Authentication Provider {#setup-auth-provider} First, let's install the required dependencies. Start in your `custom-auth` directory and ensure you have the `langgraph-cli` installed: @@ -153,30 +152,29 @@ Let's test this with a real user account! ## Testing Authentication Flow -Create a new file `create_users.py`. This will stand-in for a frontend that lets users sign up and log in. +Let's test out our new authentication flow. You can run the following code in a file or notebook. ```python -import argparse -import asyncio import os - -import dotenv import httpx +from getpass import getpass from langgraph_sdk import get_client -dotenv.load_dotenv() # Get email from command line -parser = argparse.ArgumentParser() -parser.add_argument("email", help="Your email address for testing") -args = parser.parse_args() - -base_email = args.email.split("@") +email = getpass("Enter your email: ") +base_email = email.split("@") +password = "secure-password" # CHANGEME email1 = f"{base_email[0]}+1@{base_email[1]}" email2 = f"{base_email[0]}+2@{base_email[1]}" -SUPABASE_URL = os.environ["SUPABASE_URL"] -SUPABASE_SERVICE_KEY = os.environ["SUPABASE_SERVICE_KEY"] +SUPABASE_URL = os.environ.get("SUPABASE_URL") +if not SUPABASE_URL: + SUPABASE_URL = getpass("Enter your Supabase project URL: ") + +SUPABASE_SERVICE_KEY = os.environ.get("SUPABASE_SERVICE_KEY") +if not SUPABASE_SERVICE_KEY: + SUPABASE_SERVICE_KEY = getpass("Enter your Supabase service role key: ") async def sign_up(email: str, password: str): @@ -190,55 +188,30 @@ async def sign_up(email: str, password: str): assert response.status_code == 200 return response.json() -async def main(): - # Create two test users - password = "secure-password" # CHANGEME - print(f"Creating test users: {email1} and {email2}") - await sign_up(email1, password) - await sign_up(email2, password) - -if __name__ == "__main__": - asyncio.run(main()) +# Create two test users +print(f"Creating test users: {email1} and {email2}") +await sign_up(email1, password) +await sign_up(email2, password) ``` -Then run the setup script: - -```shell -python create_users.py CHANGEME@example.com -``` +Then run the code. !!! tip "About test emails" We'll create two test accounts by adding "+1" and "+2" to your email. For example, if you use "myemail@gmail.com", we'll create "myemail+1@gmail.com" and "myemail+2@gmail.com". All emails will be delivered to your original address. -⚠️ Before continuing: Check your email and click both confirmation links. This would normally be handled by your frontend. +⚠️ Before continuing: Check your email and click both confirmation links. -Now let's test that users can only see their own data. Create a new file `test_oauth.py`. This will stand-in for your application's frontend. +Now let's test that users can only see their own data. Make sure the server is running (run `langgraph dev`) before proceeding. The following snippet requires the "anon public" key that you copied from the Supabase dashboard while [setting up the auth provider](#setup-auth-provider) previously. ```python -import argparse -import asyncio import os - -import dotenv import httpx -from langgraph_sdk import get_client - -dotenv.load_dotenv() -# Get email from command line -parser = argparse.ArgumentParser() -parser.add_argument("email", help="Your email address for testing") -args = parser.parse_args() - -# Create two test emails from the base email -base_email = args.email.split("@") -email1 = f"{base_email[0]}+1@{base_email[1]}" -email2 = f"{base_email[0]}+2@{base_email[1]}" - -# Initialize auth provider settings -SUPABASE_URL = os.environ["SUPABASE_URL"] -SUPABASE_ANON_KEY = os.environ["SUPABASE_ANON_KEY"] +from langgraph_sdk import get_client +SUPABASE_ANON_KEY = os.environ.get("SUPABASE_ANON_KEY") +if not SUPABASE_ANON_KEY: + SUPABASE_ANON_KEY = getpass("Enter your Supabase anon key: ") async def login(email: str, password: str): """Get an access token for an existing user.""" @@ -260,49 +233,37 @@ async def login(email: str, password: str): raise ValueError(f"Login failed: {response.status_code} - {response.text}") -async def main(): - password = "secure-password" - - # Log in as user 1 - user1_token = await login(email1, password) - user1_client = get_client( - url="http://localhost:2024", headers={"Authorization": f"Bearer {user1_token}"} - ) - - # Create a thread as user 1 - thread = await user1_client.threads.create() - print(f"✅ User 1 created thread: {thread['thread_id']}") - - # Try to access without a token - unauthenticated_client = get_client(url="http://localhost:2024") - try: - await unauthenticated_client.threads.create() - print("❌ Unauthenticated access should fail!") - except Exception as e: - print("✅ Unauthenticated access blocked:", e) - - # Try to access user 1's thread as user 2 - user2_token = await login(email2, password) - user2_client = get_client( - url="http://localhost:2024", headers={"Authorization": f"Bearer {user2_token}"} - ) - - try: - await user2_client.threads.get(thread["thread_id"]) - print("❌ User 2 shouldn't see User 1's thread!") - except Exception as e: - print("✅ User 2 blocked from User 1's thread:", e) - - -if __name__ == "__main__": - asyncio.run(main()) -``` - -Fetch the SUPABASE_ANON_KEY that you copied from the Supabase dashboard in step (1), then run the test. Make sure the server is running (if you have run `langgraph dev`): - -```bash -python test_oauth.py CHANGEME@example.com +# Log in as user 1 +user1_token = await login(email1, password) +user1_client = get_client( + url="http://localhost:2024", headers={"Authorization": f"Bearer {user1_token}"} +) + +# Create a thread as user 1 +thread = await user1_client.threads.create() +print(f"✅ User 1 created thread: {thread['thread_id']}") + +# Try to access without a token +unauthenticated_client = get_client(url="http://localhost:2024") +try: + await unauthenticated_client.threads.create() + print("❌ Unauthenticated access should fail!") +except Exception as e: + print("✅ Unauthenticated access blocked:", e) + +# Try to access user 1's thread as user 2 +user2_token = await login(email2, password) +user2_client = get_client( + url="http://localhost:2024", headers={"Authorization": f"Bearer {user2_token}"} +) + +try: + await user2_client.threads.get(thread["thread_id"]) + print("❌ User 2 shouldn't see User 1's thread!") +except Exception as e: + print("✅ User 2 blocked from User 1's thread:", e) ``` +The output should look like this: > ➜ custom-auth SUPABASE_ANON_KEY=eyJh... python test_oauth.py CHANGEME@example.com > ✅ User 1 created thread: d6af3754-95df-4176-aa10-dbd8dca40f1a diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md index 85910dbd4..31d9f94af 100644 --- a/docs/docs/tutorials/auth/getting_started.md +++ b/docs/docs/tutorials/auth/getting_started.md @@ -1,6 +1,4 @@ -# Setting up Custom Authentication - -In this tutorial, we will build a chatbot that only lets specific users access it. We'll start with the LangGraph template and add token-based security step by step. By the end, you'll have a working chatbot that checks for valid tokens before allowing access. +# Setting up Custom Authentication (Part 1/3) !!! note "This is part 1 of our authentication series:" @@ -8,6 +6,8 @@ In this tutorial, we will build a chatbot that only lets specific users access i 2. [Resource Authorization](resource_auth.md) - Let users have private conversations 3. [Production Auth](add_auth_server.md) - Add real user accounts and validate using OAuth2 +In this tutorial, we will build a chatbot that only lets specific users access it. We'll start with the LangGraph template and add token-based security step by step. By the end, you'll have a working chatbot that checks for valid tokens before allowing access. + ## Setting up our project First, let's create a new chatbot using the LangGraph starter template: @@ -23,22 +23,22 @@ The template gives us a placeholder LangGraph app. Let's try it out by installin pip install -e . langgraph dev ``` +If everything works, the server should start and open the studio in your 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 > > This in-memory server is designed for development and testing. > For production use, please use LangGraph Cloud. - -If everything works, the server should start and open the studio in your browser. - -Now that we've seen the base LangGraph app, let's add authentication to it! +Now that we've seen the base LangGraph app, let's add authentication to it! In part 1, we will start with a hard-coded token for illustration purposes. +We will get to a "production-ready" authentication scheme in part 3, after mastering the basics. ## Adding Authentication The [`Auth`](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) object lets you register an authentication function that the LangGraph platform will run on every request. This function receives each request and decides whether to accept or reject. -Create a new file `src/security/auth.py`. This is where we'll our code will live to check if users are allowed to access our bot: +Create a new file `src/security/auth.py`. This is where our code will live to check if users are allowed to access our bot: ```python from langgraph_sdk import Auth @@ -72,7 +72,7 @@ async def get_current_user(authorization: str | None) -> Auth.types.MinimalUserD Notice that our authentication handler does two important things: 1. Checks if a valid token is provided -2. Returns the user's +2. Returns the user's identity Now tell LangGraph to use our authentication by adding the following to the `langgraph.json` configuration: @@ -84,7 +84,7 @@ Now tell LangGraph to use our authentication by adding the following to the `lan } ``` -## Testing Our Secure Bot +## Testing Our "Secure" Bot Let's start the server again to test everything out! @@ -99,8 +99,8 @@ langgraph dev --no-browser ```json { "auth": { - "path": "src/security/auth.py:auth", - "disable_studio_auth": "true" + "path": "src/security/auth.py:auth", + "disable_studio_auth": "true" } } ``` diff --git a/docs/docs/tutorials/auth/resource_auth.md b/docs/docs/tutorials/auth/resource_auth.md index ec564aac7..adda07adf 100644 --- a/docs/docs/tutorials/auth/resource_auth.md +++ b/docs/docs/tutorials/auth/resource_auth.md @@ -1,6 +1,4 @@ -# 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. +# Making Conversations Private (Part 2/3) !!! note "This is part 2 of our authentication series:" @@ -8,6 +6,8 @@ In this tutorial, we will extend our chatbot to give each user their own private 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 +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. + ## 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). @@ -267,4 +267,4 @@ 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 +3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth) for details about the interfaces and methods used in this tutorial diff --git a/libs/sdk-py/langgraph_sdk/auth/exceptions.py b/libs/sdk-py/langgraph_sdk/auth/exceptions.py index bee227e0d..cd3019259 100644 --- a/libs/sdk-py/langgraph_sdk/auth/exceptions.py +++ b/libs/sdk-py/langgraph_sdk/auth/exceptions.py @@ -16,11 +16,6 @@ class HTTPException(Exception): headers (typing.Mapping[str, str] | None, optional): Additional HTTP headers to include in the error response. - Attributes: - status_code (int): The HTTP status code of the error - detail (str): The error message or description - headers (typing.Mapping[str, str] | None): Additional HTTP headers - Example: Default: ```python @@ -53,19 +48,9 @@ def __init__( self.headers = headers def __str__(self) -> str: - """Return a string representation of the HTTP exception. - - Returns: - str: A string in the format 'status_code: detail' - """ return f"{self.status_code}: {self.detail}" def __repr__(self) -> str: - """Return a detailed string representation of the HTTP exception. - - Returns: - str: A string representation showing the class name and all attributes - """ class_name = self.__class__.__name__ return f"{class_name}(status_code={self.status_code!r}, detail={self.detail!r})" diff --git a/libs/sdk-py/langgraph_sdk/auth/types.py b/libs/sdk-py/langgraph_sdk/auth/types.py index 2320586b9..100076dcc 100644 --- a/libs/sdk-py/langgraph_sdk/auth/types.py +++ b/libs/sdk-py/langgraph_sdk/auth/types.py @@ -55,29 +55,39 @@ - reject: Reject the operation """ -FilterType = typing.Union[ +FilterTypeFilterType = typing.Union[ typing.Dict[ str, typing.Union[str, typing.Dict[typing.Literal["$eq", "$contains"], str]] ], typing.Dict[str, str], ] -"""Type for filtering queries. +"""# +Type for filtering queries. Supports exact matches and operators: - - Simple match: {"field": "value"} - - Equals: {"field": {"$eq": "value"}} + - Exact match shorthand: {"field": "value"} + - Exact match: {"field": {"$eq": "value"}} - Contains: {"field": {"$contains": "value"}} ???+ example "Examples" + Simple exact match filter for the resource owner: ```python - # Simple match - filter = {"status": "pending"} + filter = {"owner": "user-abcd123"} + ``` - # Equals operator - filter = {"status": {"$eq": "success"}} + Explicit version of the exact match filter: + ```python + filter = {"owner": {"$eq": "user-abcd123"}} + ``` - # Contains operator - filter = {"metadata.tags": {"$contains": "important"}} + Containment: + ```python + filter = {"participants": {"$contains": "user-abcd123"}} + ``` + + Combining filters (treated as a logical `AND`): + ```python + filter = {"owner": "user-abcd123", "participants": {"$contains": "user-efgh456"}} ``` """ @@ -109,9 +119,9 @@ HandlerResult = typing.Union[None, bool, FilterType] """The result of a handler can be: -- None | True: accept the request. -- False: reject the request with a 403 error -- FilterType: filter to apply + * None | True: accept the request. + * False: reject the request with a 403 error + * FilterType: filter to apply """ Handler = Callable[..., Awaitable[HandlerResult]] From 6954e63671076429d4c15e341b0811625566bf24 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:16:35 -0800 Subject: [PATCH 24/29] Numbering --- docs/docs/how-tos/auth/custom_auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/how-tos/auth/custom_auth.md b/docs/docs/how-tos/auth/custom_auth.md index 9ac43d579..456b893b5 100644 --- a/docs/docs/how-tos/auth/custom_auth.md +++ b/docs/docs/how-tos/auth/custom_auth.md @@ -67,7 +67,7 @@ In your `langgraph.json`, add the path to your auth file: } ``` -## Connect from the client +## 3. Connect from the client Once you've set up authentication in your server, requests must include the the required authorization information based on your chosen scheme. Assuming you are using JWT token authentication, you could access your deployments using any of the following methods: From 948027aef230e8ad4dab00203be1ec7deda1696a Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:22:15 -0800 Subject: [PATCH 25/29] Notebook style --- docs/docs/tutorials/auth/add_auth_server.md | 7 +- docs/docs/tutorials/auth/getting_started.md | 57 ++++++------- docs/docs/tutorials/auth/resource_auth.md | 92 ++++++++++----------- 3 files changed, 74 insertions(+), 82 deletions(-) diff --git a/docs/docs/tutorials/auth/add_auth_server.md b/docs/docs/tutorials/auth/add_auth_server.md index fe99803af..8aa4323da 100644 --- a/docs/docs/tutorials/auth/add_auth_server.md +++ b/docs/docs/tutorials/auth/add_auth_server.md @@ -152,7 +152,10 @@ Let's test this with a real user account! ## Testing Authentication Flow -Let's test out our new authentication flow. You can run the following code in a file or notebook. +Let's test out our new authentication flow. You can run the following code in a file or notebook. You will need to provide: +- A valid email address +- A Supabase project URL (from [above](#setup-auth-provider)) +- A Supabase service role key (also from [above](#setup-auth-provider)) ```python import os @@ -199,7 +202,7 @@ Then run the code. !!! tip "About test emails" We'll create two test accounts by adding "+1" and "+2" to your email. For example, if you use "myemail@gmail.com", we'll create "myemail+1@gmail.com" and "myemail+2@gmail.com". All emails will be delivered to your original address. -⚠️ Before continuing: Check your email and click both confirmation links. +⚠️ Before continuing: Check your email and click both confirmation links. Supabase will will reject `/login` requests until after you have confirmed your users' email. Now let's test that users can only see their own data. Make sure the server is running (run `langgraph dev`) before proceeding. The following snippet requires the "anon public" key that you copied from the Supabase dashboard while [setting up the auth provider](#setup-auth-provider) previously. diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md index 31d9f94af..0679f304a 100644 --- a/docs/docs/tutorials/auth/getting_started.md +++ b/docs/docs/tutorials/auth/getting_started.md @@ -105,45 +105,38 @@ langgraph dev --no-browser } ``` -Now let's try to chat with our bot. Create a new file `test_auth.py`: +Now let's try to chat with our bot. Run the following code in a file or notebook: ```python -import asyncio from langgraph_sdk import get_client - -async def test_auth(): - # Try without a token (should fail) - client = get_client(url="http://localhost:2024") - try: - thread = await client.threads.create() - print("❌ Should have failed without token!") - except Exception as e: - print("✅ Correctly blocked access:", e) - - # Try with a valid token - client = get_client( - url="http://localhost:2024", headers={"Authorization": "Bearer user1-token"} - ) - - # Create a thread and chat +# Try without a token (should fail) +client = get_client(url="http://localhost:2024") +try: thread = await client.threads.create() - print(f"✅ Created thread as Alice: {thread['thread_id']}") - - response = await client.runs.create( - thread_id=thread["thread_id"], - assistant_id="agent", - input={"messages": [{"role": "user", "content": "Hello!"}]}, - ) - print("✅ Bot responded:") - print(response) - - -if __name__ == "__main__": - asyncio.run(test_auth()) + print("❌ Should have failed without token!") +except Exception as e: + print("✅ Correctly blocked access:", e) + +# Try with a valid token +client = get_client( + url="http://localhost:2024", headers={"Authorization": "Bearer user1-token"} +) + +# Create a thread and chat +thread = await client.threads.create() +print(f"✅ Created thread as Alice: {thread['thread_id']}") + +response = await client.runs.create( + thread_id=thread["thread_id"], + assistant_id="agent", + input={"messages": [{"role": "user", "content": "Hello!"}]}, +) +print("✅ Bot responded:") +print(response) ``` -Run the test code and you should see that: +You should see that: 1. Without a valid token, we can't access the bot 2. With a valid token, we can create threads and chat diff --git a/docs/docs/tutorials/auth/resource_auth.md b/docs/docs/tutorials/auth/resource_auth.md index adda07adf..b5b67e812 100644 --- a/docs/docs/tutorials/auth/resource_auth.md +++ b/docs/docs/tutorials/auth/resource_auth.md @@ -87,58 +87,54 @@ Notice that our simple handler does two things: ## Testing Private Conversations -Let's test our authorization. Create a new file `test_private.py`: +Let's test our authorization. If we have set things up correctly, we should expect to see all ✅ messages. Be sure to have your development server running (run `langgraph dev`): ```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()) +# 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") + ``` Run the test code and you should see output like this: From 2e1971baf3352e4587c2000d03f5f5e7d4bc36d1 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:25:25 -0800 Subject: [PATCH 26/29] Remaining feedback --- docs/docs/how-tos/auth/openapi_security.md | 85 +++++++++++----------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/docs/docs/how-tos/auth/openapi_security.md b/docs/docs/how-tos/auth/openapi_security.md index 81bb201bd..87e1e0229 100644 --- a/docs/docs/how-tos/auth/openapi_security.md +++ b/docs/docs/how-tos/auth/openapi_security.md @@ -1,8 +1,11 @@ # How to document API authentication in OpenAPI -This guide shows how to customize the OpenAPI security schema for your LangGraph Platform API documentation. This only updates the OpenAPI specification to document your security requirements - to implement the actual authentication logic, see [How to add custom authentication](./custom_auth.md). +This guide shows how to customize the OpenAPI security schema for your LangGraph Platform API documentation. A well-documented security schema helps API consumers understand how to authenticate with your API and even enables automatic client generation. See the [Authentication & Access Control conceptual guide](../../concepts/auth.md) for more details about LangGraph's authentication system. -This guide applies to all LangGraph Platform deployments (Cloud, BYOC, and self-hosted). It does not apply to isolated usage of the LangGraph open source library in your own custom server. +!!! note "Implementation vs Documentation" + This guide only covers how to document your security requirements in OpenAPI. To implement the actual authentication logic, see [How to add custom authentication](./custom_auth.md). + +This guide applies to all LangGraph Platform deployments (Cloud, BYOC, and self-hosted). It does not apply to usage of the LangGraph open source library if you are not using LangGraph Platform. ## Default Schema @@ -37,54 +40,54 @@ Note that LangGraph Platform does not provide authentication endpoints - you'll === "OAuth2 with Bearer Token" -```json -{ - "auth": { - "path": "./auth.py:my_auth", // Implement auth logic here - "openapi": { - "securitySchemes": { - "OAuth2": { - "type": "oauth2", - "flows": { - "implicit": { - "authorizationUrl": "https://your-auth-server.com/oauth/authorize", - "scopes": { - "me": "Read information about the current user", - "threads": "Access to create and manage threads" + ```json + { + "auth": { + "path": "./auth.py:my_auth", // Implement auth logic here + "openapi": { + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://your-auth-server.com/oauth/authorize", + "scopes": { + "me": "Read information about the current user", + "threads": "Access to create and manage threads" + } + } } } - } + }, + "security": [ + {"OAuth2": ["me", "threads"]} + ] } - }, - "security": [ - {"OAuth2": ["me", "threads"]} - ] + } } - } -} -``` + ``` === "API Key" -```json -{ - "auth": { - "path": "./auth.py:my_auth", // Implement auth logic here - "openapi": { - "securitySchemes": { - "apiKeyAuth": { - "type": "apiKey", - "in": "header", - "name": "X-API-Key" + ```json + { + "auth": { + "path": "./auth.py:my_auth", // Implement auth logic here + "openapi": { + "securitySchemes": { + "apiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + }, + "security": [ + {"apiKeyAuth": []} + ] } - }, - "security": [ - {"apiKeyAuth": []} - ] + } } - } -} -``` + ``` ## Testing From 06e1e4ed12f5bb7783e37585a89d4dcb07e63409 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:52:02 -0800 Subject: [PATCH 27/29] Update cross-linking --- docs/_scripts/notebook_hooks.py | 6 ++++ docs/docs/cloud/reference/cli.md | 2 +- docs/docs/concepts/auth.md | 35 +++++++++++---------- docs/docs/tutorials/auth/getting_started.md | 22 ++++++++----- libs/sdk-py/langgraph_sdk/auth/types.py | 10 +++++- 5 files changed, 48 insertions(+), 27 deletions(-) diff --git a/docs/_scripts/notebook_hooks.py b/docs/_scripts/notebook_hooks.py index 23c460e7a..c993b6d22 100644 --- a/docs/_scripts/notebook_hooks.py +++ b/docs/_scripts/notebook_hooks.py @@ -1,4 +1,5 @@ import logging +import os from typing import Any, Dict from mkdocs.structure.pages import Page @@ -8,6 +9,7 @@ logger = logging.getLogger(__name__) logging.basicConfig() logger.setLevel(logging.INFO) +DISABLED = os.getenv("DISABLE_NOTEBOOK_CONVERT") in ("1", "true", "True") class NotebookFile(File): @@ -16,6 +18,8 @@ def is_documentation_page(self): def on_files(files: Files, **kwargs: Dict[str, Any]): + if DISABLED: + return files new_files = Files([]) for file in files: if file.src_path.endswith(".ipynb"): @@ -32,6 +36,8 @@ def on_files(files: Files, **kwargs: Dict[str, Any]): def on_page_markdown(markdown: str, page: Page, **kwargs: Dict[str, Any]): + if DISABLED: + return markdown if page.file.src_path.endswith(".ipynb"): logger.info("Processing Jupyter notebook: %s", page.file.src_path) body = convert_notebook(page.file.abs_src_path) diff --git a/docs/docs/cloud/reference/cli.md b/docs/docs/cloud/reference/cli.md index 72599e7fc..1daff19ee 100644 --- a/docs/docs/cloud/reference/cli.md +++ b/docs/docs/cloud/reference/cli.md @@ -21,7 +21,7 @@ The LangGraph command line interface includes commands to build and run a LangGr [](){#langgraph.json} -## Configuration File +## Configuration File {#configuration-file} The LangGraph CLI requires a JSON configuration file with the following keys: diff --git a/docs/docs/concepts/auth.md b/docs/docs/concepts/auth.md index c14135c93..ce0617cb9 100644 --- a/docs/docs/concepts/auth.md +++ b/docs/docs/concepts/auth.md @@ -23,23 +23,24 @@ A typical authentication setup involves three main components: 1. **Authentication Provider** (Identity Provider/IdP) - - A dedicated service that manages user identities and credentials - - Examples: Auth0, Supabase Auth, Okta, or your own auth server - - Handles user registration, login, password resets, etc. - - Issues tokens (JWT, session tokens, etc.) after successful authentication + * A dedicated service that manages user identities and credentials + * Handles user registration, login, password resets, etc. + * Issues tokens (JWT, session tokens, etc.) after successful authentication + * Examples: Auth0, Supabase Auth, Okta, or your own auth server 2. **LangGraph Backend** (Resource Server) - - Your LangGraph application that contains business logic and protected resources - - Validates tokens with the auth provider - - Enforces access control based on user identity and permissions - - Never stores user credentials directly + * Your LangGraph application that contains business logic and protected resources + * Validates tokens with the auth provider + * Enforces access control based on user identity and permissions + * Doesn't store user credentials directly 3. **Client Application** (Frontend) - - Web app, mobile app, or API client - - Collects user credentials and sends to auth provider - - Receives tokens from auth provider - - Includes tokens in requests to LangGraph backend + + * Web app, mobile app, or API client + * Collects time-sensitive user credentials and sends to auth provider + * Receives tokens from auth provider + * Includes these tokens in requests to LangGraph backend Here's how these components typically interact: @@ -58,15 +59,15 @@ sequenceDiagram LG-->>Client: 7. Return resources ``` -Your `@auth.authenticate` handler in LangGraph handles steps 4-5, while your `@auth.on` handlers implement step 6. +Your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler in LangGraph handles steps 4-5, while your [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) handlers implement step 6. ## Authentication -Authentication in LangGraph runs as middleware on every request. Your `@auth.authenticate` handler receives request information and must: +Authentication in LangGraph runs as middleware on every request. Your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler receives request information and should: 1. Validate the credentials 2. Return user information if valid -3. Raise an HTTP exception if invalid (or AssertionError) +3. Raise an HTTP exception or AssertionError if invalid ```python from langgraph_sdk import Auth @@ -98,12 +99,12 @@ async def authenticate(headers: dict) -> Auth.types.MinimalUserDict: The returned user information is available: -- To your authorization handlers via `ctx.user` +- To your authorization handlers via [`ctx.user`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.AuthContext) - In your application via `config["configuration"]["langgraph_auth_user"]` ## Authorization -After authentication, LangGraph calls your `@auth.on` handlers to control access to specific resources (e.g., threads, assistants, crons). These handlers can: +After authentication, LangGraph calls your [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) handlers to control access to specific resources (e.g., threads, assistants, crons). These handlers can: 1. Add metadata to be saved during resource creation by mutating the `value["metadata"]` dictionary directly. 2. Filter resources by metadata during search/list or read operations by returning a [filter dictionary](#filter-operations). diff --git a/docs/docs/tutorials/auth/getting_started.md b/docs/docs/tutorials/auth/getting_started.md index 0679f304a..c8df6592d 100644 --- a/docs/docs/tutorials/auth/getting_started.md +++ b/docs/docs/tutorials/auth/getting_started.md @@ -31,6 +31,7 @@ If everything works, the server should start and open the studio in your browser > > This in-memory server is designed for development and testing. > For production use, please use LangGraph Cloud. + Now that we've seen the base LangGraph app, let's add authentication to it! In part 1, we will start with a hard-coded token for illustration purposes. We will get to a "production-ready" authentication scheme in part 3, after mastering the basics. @@ -49,9 +50,12 @@ VALID_TOKENS = { "user2-token": {"id": "user2", "name": "Bob"}, } +# The "Auth" object is a container that LangGraph will use to mark our authentication function auth = Auth() +# The `authenticate` decorator tells LangGraph to call this function as middleware +# for every request. This will determine whether the request is allowed or not @auth.authenticate async def get_current_user(authorization: str | None) -> Auth.types.MinimalUserDict: """Check if the user's token is valid.""" @@ -69,12 +73,12 @@ async def get_current_user(authorization: str | None) -> Auth.types.MinimalUserD } ``` -Notice that our authentication handler does two important things: +Notice that our [authentication](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler does two important things: -1. Checks if a valid token is provided -2. Returns the user's identity +1. Checks if a valid token is provided in the request's [Authorization header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) +2. Returns the user's [identity](../../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.MinimalUserDict) -Now tell LangGraph to use our authentication by adding the following to the `langgraph.json` configuration: +Now tell LangGraph to use our authentication by adding the following to the [`langgraph.json`](../../cloud/reference/cli.md#configuration-file) configuration: ```json { @@ -137,14 +141,16 @@ print(response) ``` You should see that: + 1. Without a valid token, we can't access the bot 2. With a valid token, we can create threads and chat -Congratulations! You've built a chatbot that only lets "authorized" users access it. While this system doesn't (yet) implement a production-ready security scheme, we've learned the basic mechanics of how to control access to our bot. In the next tutorial, we'll learn how to give each user their own private conversations. +Congratulations! You've built a chatbot that only lets "authenticated" users access it. While this system doesn't (yet) implement a production-ready security scheme, we've learned the basic mechanics of how to control access to our bot. In the next tutorial, we'll learn how to give each user their own private conversations. ## What's Next? Now that you can control who accesses your bot, you might want to: -1. Move on to [Resource Authorization](resource_auth.md) to learn how to make conversations private -2. Read more about [authentication concepts](../../concepts/auth.md) -3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md) for more authentication options \ No newline at end of file + +1. Continue the tutorial by going to [Making Conversations Private (Part 2/3)](resource_auth.md) to learn about resource authorization. +2. Read more about [authentication concepts](../../concepts/auth.md). +3. Check out the [API reference](../../cloud/reference/sdk/python_sdk_ref.md) for more authentication details. \ No newline at end of file diff --git a/libs/sdk-py/langgraph_sdk/auth/types.py b/libs/sdk-py/langgraph_sdk/auth/types.py index 100076dcc..2d00aed02 100644 --- a/libs/sdk-py/langgraph_sdk/auth/types.py +++ b/libs/sdk-py/langgraph_sdk/auth/types.py @@ -153,12 +153,20 @@ def identity(self) -> str: class MinimalUserDict(typing.TypedDict, total=False): - """The minimal user dictionary.""" + """The dictionary representation of a user.""" identity: typing_extensions.Required[str] + """The required unique identifier for the user.""" display_name: str + """The optional display name for the user.""" is_authenticated: bool + """Whether the user is authenticated. Defaults to True.""" permissions: Sequence[str] + """A list of permissions associated with the user. + + You can use these in your `@auth.on` authorization logic to determine + access permissions to different resources. + """ @typing.runtime_checkable From d4a1fe5a032227d194f513fc7bb59ebd69c56f04 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:03:13 -0800 Subject: [PATCH 28/29] concept --- docs/docs/concepts/auth.md | 43 ++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/docs/docs/concepts/auth.md b/docs/docs/concepts/auth.md index ce0617cb9..974308a63 100644 --- a/docs/docs/concepts/auth.md +++ b/docs/docs/concepts/auth.md @@ -12,8 +12,8 @@ LangGraph Platform provides a flexible authentication and authorization system t While often used interchangeably, these terms represent distinct security concepts: -- **Authentication** ("AuthN") verifies _who_ you are. This runs as middleware for every request. -- **Authorization** ("AuthZ") determines _what you can do_. This validates the user's privileges and roles on a per-resource basis. +- [**Authentication**](#authentication) ("AuthN") verifies _who_ you are. This runs as middleware for every request. +- [**Authorization**](#authorization) ("AuthZ") determines _what you can do_. This validates the user's privileges and roles on a per-resource basis. In LangGraph Platform, authentication is handled by your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler, and authorization is handled by your [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) handlers. @@ -53,21 +53,22 @@ sequenceDiagram Client->>Auth: 1. Login (username/password) Auth-->>Client: 2. Return token Client->>LG: 3. Request with token - LG->>Auth: 4. Validate token - Auth-->>LG: 5. Confirm validity - Note over LG: 6. Apply access control - LG-->>Client: 7. Return resources + Note over LG: 4. Validate token (@auth.authenticate) + LG-->>Auth: 5. Fetch user info + Auth-->>LG: 6. Confirm validity + Note over LG: 7. Apply access control (@auth.on.*) + LG-->>Client: 8. Return resources ``` -Your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler in LangGraph handles steps 4-5, while your [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) handlers implement step 6. +Your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler in LangGraph handles steps 4-6, while your [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) handlers implement step 7. ## Authentication Authentication in LangGraph runs as middleware on every request. Your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler receives request information and should: 1. Validate the credentials -2. Return user information if valid -3. Raise an HTTP exception or AssertionError if invalid +2. Return [user info](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.MinimalUserDict) containing the user's identity and user information if valid +3. Raise an [HTTP exception](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.exceptions.HTTPException) or AssertionError if invalid ```python from langgraph_sdk import Auth @@ -102,6 +103,22 @@ The returned user information is available: - To your authorization handlers via [`ctx.user`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.AuthContext) - In your application via `config["configuration"]["langgraph_auth_user"]` +??? tip "Supported Parameters" + + The [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler can accept any of the following parameters by name: + + * request (Request): The raw ASGI request object + * body (dict): The parsed request body + * path (str): The request path, e.g., "/threads/abcd-1234-abcd-1234/runs/abcd-1234-abcd-1234/stream" + * method (str): The HTTP method, e.g., "GET" + * path_params (dict[str, str]): URL path parameters, e.g., {"thread_id": "abcd-1234-abcd-1234", "run_id": "abcd-1234-abcd-1234"} + * query_params (dict[str, str]): URL query parameters, e.g., {"stream": "true"} + * headers (dict[bytes, bytes]): Request headers + * authorization (str | None): The Authorization header value (e.g., "Bearer ") + + In many of our tutorials, we will just show the "authorization" parameter to be concise, but you can opt to accept more information as needed + to implement your custom authentication scheme. + ## Authorization After authentication, LangGraph calls your [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) handlers to control access to specific resources (e.g., threads, assistants, crons). These handlers can: @@ -110,7 +127,7 @@ After authentication, LangGraph calls your [`@auth.on`](../cloud/reference/sdk/p 2. Filter resources by metadata during search/list or read operations by returning a [filter dictionary](#filter-operations). 3. Raise an HTTP exception if access is denied. -If you want to just implement simple user-scoped access control, you can use a single `@auth.on` handler for all resources and actions. If you want to have different control depending on the resource and action, you can use [resource-specific handlers](#resource-specific-handlers). See the [Supported Resources](#supported-resources) section for a full list of the resources that support access control. +If you want to just implement simple user-scoped access control, you can use a single [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) handler for all resources and actions. If you want to have different control depending on the resource and action, you can use [resource-specific handlers](#resource-specific-handlers). See the [Supported Resources](#supported-resources) section for a full list of the resources that support access control. ```python @auth.on @@ -154,7 +171,7 @@ async def add_owner( ### Resource-Specific Handlers {#resource-specific-handlers} -You can register handlers for specific resources and actions by chaining the resource and action names together with the `@auth.on` decorator. +You can register handlers for specific resources and actions by chaining the resource and action names together with the [`@auth.on`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.on) decorator. When a request is made, the most specific handler that matches that resource and action is called. Below is an example of how to register handlers for specific resources and actions. For the following setup: 1. Authenticated users are able to create threads, read thread, create runs on threads @@ -410,5 +427,5 @@ LangGraph Platform provides different security defaults: For implementation details: -- [Setting up authentication](../tutorials/auth/getting_started.md) -- [Custom auth handlers](../how-tos/auth/custom_auth.md) +- Check out the introductory tutorial on [setting up authentication](../tutorials/auth/getting_started.md) +- See the how-to guide on implementing a [custom auth handlers](../how-tos/auth/custom_auth.md) From 1709cd3fb5141791eefb39c0022f46aa9a0ffedf Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:08:19 -0800 Subject: [PATCH 29/29] concept --- libs/sdk-py/langgraph_sdk/auth/types.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/sdk-py/langgraph_sdk/auth/types.py b/libs/sdk-py/langgraph_sdk/auth/types.py index 2d00aed02..b057aeb84 100644 --- a/libs/sdk-py/langgraph_sdk/auth/types.py +++ b/libs/sdk-py/langgraph_sdk/auth/types.py @@ -55,14 +55,13 @@ - reject: Reject the operation """ -FilterTypeFilterType = typing.Union[ +FilterType = typing.Union[ typing.Dict[ str, typing.Union[str, typing.Dict[typing.Literal["$eq", "$contains"], str]] ], typing.Dict[str, str], ] -"""# -Type for filtering queries. +"""Response type for authorization handlers. Supports exact matches and operators: - Exact match shorthand: {"field": "value"}