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]]