diff --git a/Makefile b/Makefile index 0d039591e..9bd2847e4 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ serve-clean-docs: clean-docs poetry run python -m mkdocs serve -c -f docs/mkdocs.yml --strict -w ./libs/langgraph serve-docs: build-typedoc - poetry run python -m mkdocs serve -f docs/mkdocs.yml -w ./libs/langgraph -w ./libs/checkpoint --dirty + poetry run python -m mkdocs serve -f docs/mkdocs.yml -w ./libs/langgraph -w ./libs/checkpoint -w ./libs/sdk-py --dirty clean-docs: find ./docs/docs -name "*.ipynb" -type f -delete diff --git a/docs/docs/cloud/reference/sdk/python_sdk_ref.md b/docs/docs/cloud/reference/sdk/python_sdk_ref.md index 0492a08cd..4126ca306 100644 --- a/docs/docs/cloud/reference/sdk/python_sdk_ref.md +++ b/docs/docs/cloud/reference/sdk/python_sdk_ref.md @@ -6,3 +6,25 @@ ::: langgraph_sdk.schema handler: python + + +::: 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 + handler: python + +::: langgraph_sdk.auth.exceptions + handler: python \ No newline at end of file diff --git a/libs/sdk-py/langgraph_sdk/auth/__init__.py b/libs/sdk-py/langgraph_sdk/auth/__init__.py index 5c34a8976..317c89e9f 100644 --- a/libs/sdk-py/langgraph_sdk/auth/__init__.py +++ b/libs/sdk-py/langgraph_sdk/auth/__init__.py @@ -11,26 +11,54 @@ class Auth: - """Authentication and authorization management for LangGraph. + """Add custom authentication and authorization management to your LangGraph application. The Auth class provides a unified system for handling authentication and - authorization in LangGraph applications. It supports: - - 1. Authentication via a decorator-based handler system - 2. Fine-grained authorization rules for different resources and actions - 3. Global and resource-specific authorization handlers + authorization in LangGraph applications. It supports custom user authentication + protocols and fine-grained authorization rules for different resources and + actions. + + To use, create a separate python file and add the path to the file to your + LangGraph API configuration file (`langgraph.json`). Within that file, create + an instance of the Auth class and register authentication and authorization + handlers as needed. + + Example `langgraph.json` file: + + ```json + { + "dependencies": ["."], + "graphs": { + "agent": "./my_agent/agent.py:graph" + }, + "env": ".env", + "auth": { + "path": "./auth.py:my_auth" + } + ``` + + Then the LangGraph server will load your auth file and run it server-side whenever a request comes in. ???+ example "Basic Usage" ```python from langgraph_sdk import Auth - auth = Auth() + my_auth = Auth() + + async def verify_token(token: str) -> str: + # Verify token and return user_id + # This would typically be a call to your auth server + return "user_id" @auth.authenticate - async def authenticate(authorization: str) -> tuple[list[str], str]: - # Verify token and return (scopes, user_id) - user_id = verify_token(authorization) - return ["read", "write"], user_id + async def authenticate(authorization: str) -> str: + # Verify token and return user_id + result = await verify_token(authorization) + if result != "user_id": + raise Auth.exceptions.HTTPException( + status_code=401, detail="Unauthorized" + ) + return result # Global fallback handler @auth.on @@ -44,11 +72,12 @@ async def authorize_thread_create(params: Auth.on.threads.create.value): ``` ???+ note "Request Processing Flow" - 1. Authentication is performed first on every request + 1. Authentication (your `@auth.authenticate` handler) is performed first on **every request** 2. For authorization, the most specific matching handler is called: - - If a handler exists for the exact resource and action, it is used - - Otherwise, if a handler exists for the resource with any action, it is used - - Finally, if no specific handlers match, the global handler is used (if any) + * If a handler exists for the exact resource and action, it is used (e.g., `@auth.on.threads.create`) + * Otherwise, if a handler exists for the resource with any action, it is used (e.g., `@auth.on.threads`) + * Finally, if no specific handlers match, the global handler is used (e.g., `@auth.on`) + * If no global handler is set, the request is accepted This allows you to set default behavior with a global handler while overriding specific routes as needed. @@ -71,10 +100,64 @@ async def authorize_thread_create(params: Auth.on.threads.create.value): """Reference to auth exception definitions. Provides access to all exception definitions used in the auth system, - like HTTPException, etc.""" + like HTTPException, etc. + """ def __init__(self) -> None: self.on = _On(self) + """Entry point for authorization handlers that control access to specific resources. + + The on class provides a flexible way to define authorization rules for different + resources and actions in your application. It supports three main usage patterns: + + 1. Global handlers that run for all resources and actions + 2. Resource-specific handlers that run for all actions on a resource + 3. Resource and action specific handlers for fine-grained control + + Each handler must be an async function that accepts two parameters: + - ctx (AuthContext): Contains request context and authenticated user info + - value: The data being authorized (type varies by endpoint) + + The handler should return one of: + + - None or True: Accept the request + - False: Reject with 403 error + - FilterType: Apply filtering rules to the response + + ???+ example "Examples" + Global handler for all requests: + ```python + @auth.on + async def reject_unhandled_requests(ctx: AuthContext, value: Any) -> None: + print(f"Request to {ctx.path} by {ctx.user.identity}") + return False + ``` + + Resource-specific handler. This would take precedence over the global handler + for all actions on the `threads` resource: + ```python + @auth.on.threads + async def check_thread_access(ctx: AuthContext, value: Any) -> bool: + # Allow access only to threads created by the user + return value.get("created_by") == ctx.user.identity + ``` + + Resource and action specific handler: + ```python + @auth.on.threads.delete + async def prevent_thread_deletion(ctx: AuthContext, value: Any) -> bool: + # Only admins can delete threads + return "admin" in ctx.user.permissions + ``` + + Multiple resources or actions: + ```python + @auth.on(resources=["threads", "runs"], actions=["create", "update"]) + async def rate_limit_writes(ctx: AuthContext, value: Any) -> bool: + # Implement rate limiting for write operations + return await check_rate_limit(ctx.user.identity) + ``` + """ # These are accessed by the API. Changes to their names or types is # will be considered a breaking change. self._handlers: dict[tuple[str, str], list[types.Handler]] = {} @@ -88,21 +171,23 @@ def authenticate(self, fn: AH) -> AH: The authentication handler is responsible for verifying credentials and returning user scopes. It 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 - - method (str): The HTTP method - - scopes (list[str]): Required scopes - - path_params (dict[str, str]): URL path parameters - - query_params (dict[str, str]): URL query parameters - - headers (dict[str, bytes]): Request headers - - authorization (str): The Authorization header value + - 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 ") Args: fn (Callable): The authentication handler function to register. - Must return tuple[scopes, user] - where scopes is a list of string claims (like "runs:read", etc.) - and user is either a user object (or similar dict) or a user id string. + Must return a representation of the user. This could be a: + - string (the user id) + - dict containing {"identity": str, "permissions": list[str]} + - or an object with identity and permissions properties + Permissions can be optionally used by your handlers downstream. Returns: The registered handler function. @@ -114,21 +199,38 @@ def authenticate(self, fn: AH) -> AH: Basic token authentication: ```python @auth.authenticate - async def authenticate(authorization: str) -> tuple[list[str], str]: + async def authenticate(authorization: str) -> str: user_id = verify_token(authorization) - return ["read"], user_id + return user_id ``` - Complex authentication with request context: + Accept the full request context: ```python @auth.authenticate async def authenticate( method: str, path: str, headers: dict[str, bytes] - ) -> tuple[list[str], MinimalUser]: + ) -> str: user = await verify_request(method, path, headers) - return user.scopes, user + return user + ``` + + Return user name and permissions: + ```python + @auth.authenticate + async def authenticate( + method: str, + path: str, + headers: dict[str, bytes] + ) -> Auth.types.MinimalUserDict: + permissions, user = await verify_request(method, path, headers) + # Permissions could be things like ["runs:read", "runs:write", "threads:read", "threads:write"] + return { + "identity": user["id"], + "permissions": permissions, + "display_name": user["name"], + } ``` """ if self._authenticate_handler is not None: @@ -363,9 +465,58 @@ class _CronsOn( class _On: + """Entry point for authorization handlers that control access to specific resources. + + The _On class provides a flexible way to define authorization rules for different resources + and actions in your application. It supports three main usage patterns: + + 1. Global handlers that run for all resources and actions + 2. Resource-specific handlers that run for all actions on a resource + 3. Resource and action specific handlers for fine-grained control + + Each handler must be an async function that accepts two parameters: + - ctx (AuthContext): Contains request context and authenticated user info + - value: The data being authorized (type varies by endpoint) + + The handler should return one of: + - None or True: Accept the request + - False: Reject with 403 error + - FilterType: Apply filtering rules to the response + + ???+ example "Examples" + + Global handler for all requests: + ```python + @auth.on + async def log_all_requests(ctx: AuthContext, value: Any) -> None: + print(f"Request to {ctx.path} by {ctx.user.identity}") + return True + ``` + + Resource-specific handler: + ```python + @auth.on.threads + async def check_thread_access(ctx: AuthContext, value: Any) -> bool: + # Allow access only to threads created by the user + return value.get("created_by") == ctx.user.identity + ``` + + Resource and action specific handler: + ```python + @auth.on.threads.delete + async def prevent_thread_deletion(ctx: AuthContext, value: Any) -> bool: + # Only admins can delete threads + return "admin" in ctx.user.permissions + ``` + + Multiple resources or actions: + ```python + @auth.on(resources=["threads", "runs"], actions=["create", "update"]) + async def rate_limit_writes(ctx: AuthContext, value: Any) -> bool: + # Implement rate limiting for write operations + return await check_rate_limit(ctx.user.identity) + ``` """ - Entry point for @auth.on decorators. - Provides access to specific resources.""" __slots__ = ( "_auth", @@ -420,7 +571,9 @@ async def handler(): ... # types.Handler for thread creation return fn # Used with parameters, return a decorator - def decorator(handler: AHO) -> AHO: + def decorator( + handler: AHO, + ) -> AHO: if isinstance(resources, str): resource_list = [resources] else: diff --git a/libs/sdk-py/langgraph_sdk/auth/types.py b/libs/sdk-py/langgraph_sdk/auth/types.py index fc9117e00..cc47651d0 100644 --- a/libs/sdk-py/langgraph_sdk/auth/types.py +++ b/libs/sdk-py/langgraph_sdk/auth/types.py @@ -16,6 +16,8 @@ from datetime import datetime from uuid import UUID +import typing_extensions + RunStatus = typing.Literal["pending", "error", "success", "timeout", "interrupted"] """Status of a run execution. @@ -143,9 +145,10 @@ def identity(self) -> str: class MinimalUserDict(typing.TypedDict, total=False): """The minimal user dictionary.""" - identity: str + identity: typing_extensions.Required[str] display_name: str is_authenticated: bool + permissions: Sequence[str] @typing.runtime_checkable @@ -167,25 +170,28 @@ def identity(self) -> str: """The unique identifier for the user.""" ... + @property + def permissions(self) -> Sequence[str]: + """The permissions associated with the user.""" + ... + Authenticator = Callable[ ..., Awaitable[ - tuple[ - list[str], - typing.Union[ - MinimalUser, str, MinimalUserDict, typing.Mapping[str, typing.Any] - ], - ] + typing.Union[ + MinimalUser, str, BaseUser, MinimalUserDict, typing.Mapping[str, typing.Any] + ], ], ] """Type for authentication functions. An authenticator can return either: -1. A tuple of (scopes, MinimalUser/BaseUser) -2. A tuple of (scopes, str) where str is the user identity +1. A string (user_id) +2. A dict containing {"identity": str, "permissions": list[str]} +3. An object with identity and permissions properties -Scopes can be used downstream by your authorization logic to determine +Permissions can be used downstream by your authorization logic to determine access permissions to different resources. The authenticate decorator will automatically inject any of the following parameters @@ -196,11 +202,10 @@ def identity(self) -> str: body (dict): The parsed request body path (str): The request path method (str): The HTTP method (GET, POST, etc.) - scopes (list[str]): The required scopes for this endpoint path_params (dict[str, str] | None): URL path parameters query_params (dict[str, str] | None): URL query parameters headers (dict[str, bytes] | None): Request headers - authorization (str | None): The Authorization header value + authorization (str | None): The Authorization header value (e.g. "Bearer ") ???+ example "Examples" Basic authentication with token: @@ -210,9 +215,8 @@ def identity(self) -> str: auth = Auth() @auth.authenticate - async def authenticate1(authorization: str) -> tuple[list[str], MinimalUser]: - user = await get_user(authorization) - return ["read", "write"], user + async def authenticate1(authorization: str) -> Auth.types.MinimalUserDict: + return await get_user(authorization) ``` Authentication with multiple parameters: @@ -222,17 +226,17 @@ async def authenticate2( method: str, path: str, headers: dict[str, bytes] - ) -> tuple[list[str], str]: + ) -> Auth.types.MinimalUserDict: # Custom auth logic using method, path and headers - user_id = verify_request(method, path, headers) - return ["read"], user_id + user = verify_request(method, path, headers) + return user ``` Accepting the raw ASGI request: ```python MY_SECRET = "my-secret-key" @auth.authenticate - async def get_current_user(request: Request) -> tuple[list[str], dict]: + async def get_current_user(request: Request) -> Auth.types.MinimalUserDict: try: token = (request.headers.get("authorization") or "").split(" ", 1)[1] payload = jwt.decode(token, MY_SECRET, algorithms=["HS256"]) @@ -252,10 +256,11 @@ async def get_current_user(request: Request) -> tuple[list[str], dict]: raise HTTPException(status_code=401, detail="User not found") user_data = response.json() - return payload.get("role", []), { - "username": user_data["id"], - "email": user_data["email"], - "full_name": user_data.get("user_metadata", {}).get("full_name") + return { + "identity": user_data["id"], + "display_name": user_data.get("name"), + "permissions": user_data.get("permissions", []), + "is_authenticated": True, } ``` """ @@ -269,8 +274,8 @@ class BaseAuthContext: authorization decisions. """ - scopes: Sequence[str] - """The scopes granted to the authenticated user.""" + permissions: Sequence[str] + """The permissions granted to the authenticated user.""" user: BaseUser """The authenticated user.""" @@ -696,16 +701,18 @@ class on: ```python from langgraph_sdk import Auth - @Auth.on + auth = Auth() + + @auth.on def handle_all(params: Auth.on.value): raise Exception("Not authorized") - @Auth.on.threads.create + @auth.on.threads.create def handle_thread_create(params: Auth.on.threads.create.value): # Handle thread creation pass - @Auth.on.assistants.search + @auth.on.assistants.search def handle_assistant_search(params: Auth.on.assistants.search.value): # Handle assistant search pass diff --git a/libs/sdk-py/pyproject.toml b/libs/sdk-py/pyproject.toml index 7839c816e..c73c8afa1 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.46" +version = "0.1.47" description = "SDK for interacting with LangGraph API" authors = [] license = "MIT"