LangSmith 提供灵活的身份验证和授权系统,可以与大多数身份验证方案集成。

核心概念

身份验证 vs 授权

虽然经常互换使用,但这些术语代表不同的安全概念:
  • 身份验证(“AuthN”)验证_您是谁_。这作为每个请求的中间件运行。
  • 授权(“AuthZ”)确定_您可以做什么_。这在每个资源的基础上验证用户的特权和角色。
在 LangSmith 中,身份验证由您的 @auth.authenticate 处理程序处理,授权由您的 @auth.on 处理程序处理。

默认安全模型

LangSmith 提供不同的安全默认设置:

LangSmith

  • 默认使用 LangSmith API 密钥
  • 需要 x-api-key 头中的有效 API 密钥
  • 可以使用您的身份验证处理程序自定义
自定义身份验证 LangSmith 的所有计划都支持自定义身份验证。

自托管

  • 没有默认身份验证
  • 完全灵活地实现您的安全模型
  • 您控制身份验证和授权的所有方面

系统架构

典型的身份验证设置涉及三个主要组件:
  1. 身份验证提供商(身份提供商/IdP)
  • 管理用户身份和凭据的专用服务
  • 处理用户注册、登录、密码重置等
  • 在成功身份验证后颁发令牌(JWT、会话令牌等)
  • 示例:Auth0、Supabase Auth、Okta 或您自己的身份验证服务器
  1. LangGraph 后端(资源服务器)
  • 包含业务逻辑和受保护资源的 LangGraph 应用程序
  • 使用身份验证提供商验证令牌
  • 根据用户身份和权限强制执行访问控制
  • 不直接存储用户凭据
  1. 客户端应用程序(前端)
  • Web 应用程序、移动应用程序或 API 客户端
  • 收集具有时效性的用户凭据并发送到身份验证提供商
  • 从身份验证提供商接收令牌
  • 在对 LangGraph 后端的请求中包含这些令牌
以下是这些组件通常如何交互: 您在 LangGraph 中的 @auth.authenticate 处理程序处理步骤 4-6,而您的 @auth.on 处理程序实现步骤 7。

身份验证

LangGraph 中的身份验证在每个请求上作为中间件运行。您的 @auth.authenticate 处理程序接收请求信息并应该:
  1. 验证凭据
  2. 如果有效,返回包含用户身份和用户信息的用户信息
  3. 如果无效,抛出 HTTP 异常或 AssertionError
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(b"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"]
The @auth.authenticate handler can accept any of the following parameters by name:
  • request (Request): The raw ASGI request object
  • 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 <token>")
在我们的许多教程中,为了简洁起见,我们只会显示”authorization”参数,但您可以根据需要选择接受更多信息以实现您的自定义身份验证方案。

智能体身份验证

自定义身份验证允许委托访问。您在 @auth.authenticate 中返回的值被添加到运行上下文中,为智能体提供用户范围的凭据,使它们能够代表用户访问资源。 身份验证后,平台创建一个特殊的配置对象,该对象通过可配置的上下文传递给您的图和所有节点。 此对象包含有关当前用户的信息,包括您从 @auth.authenticate 处理程序返回的任何自定义字段。 要使智能体能够代表用户执行操作,请使用自定义身份验证中间件。这将允许智能体代表用户与外部系统(如 MCP 服务器、外部数据库,甚至其他智能体)进行交互。 有关更多信息,请参阅使用自定义身份验证指南。

使用 MCP 的智能体身份验证

有关如何将智能体验证到 MCP 服务器的信息,请参阅 MCP 概念指南

授权

身份验证后,LangGraph 调用您的 @auth.on 处理程序来控制对特定资源(例如,线程、助手、定时任务)的访问。这些处理程序可以:
  1. 通过直接修改 value["metadata"] 字典,在资源创建期间添加要保存的元数据。有关每个操作值可以采用的类型列表,请参阅支持的操作表
  2. 在搜索/列表或读取操作期间通过返回过滤字典按元数据过滤资源。
  3. 如果拒绝访问,抛出 HTTP 异常。
如果您只想实现简单的用户范围访问控制,您可以为所有资源和操作使用单个 @auth.on 处理程序。如果您想根据资源和操作进行不同的控制,可以使用特定于资源的处理程序。有关支持访问控制的资源的完整列表,请参阅支持的资源部分。
@auth.on
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

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 threads, and 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.
Supported Handlers For a full list of supported resources and actions, see the Supported Resources section below.
# 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) -> False:
    print(f"Request to {ctx.path} by {ctx.user.identity}")
    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(
    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 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
):
    # Reject if the user does not have write access
    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}

# 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 precedenceover 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}

# Assistant creation
@auth.on.assistants.create
async def on_assistant_create(
    ctx: Auth.types.AuthContext,
    value: Auth.types.assistants.create.value
):
    if "assistants:create" not in ctx.permissions:
        raise Auth.exceptions.HTTPException(
            status_code=403,
            detail="User lacks the required permissions."
        )
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.

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
A filter dictionary is a dictionary with keys that match the resource metadata. It supports three operators:
  • 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}}) or list containment (e.g., {"allowed_users": {"$contains": [user_id_1, user_id_2]}}). The value here must be an element of the list or a subset of the elements of the list, respectively. The metadata in the stored resource must be a list/container type.
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 for more information.

Common access patterns

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.
@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

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.
# 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)

Supported resources

LangGraph provides three levels of authorization handlers, from most general to most specific:
  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
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.
“Type Safety” Each handler has type hints available for its value parameter at Auth.types.on.<resource>.<action>.value. For example:
@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.

Supported actions and types

Here are all the supported action handlers:
ResourceHandlerDescriptionValue Type
Threads@auth.on.threads.createThread creationThreadsCreate
@auth.on.threads.readThread retrievalThreadsRead
@auth.on.threads.updateThread updatesThreadsUpdate
@auth.on.threads.deleteThread deletionThreadsDelete
@auth.on.threads.searchListing threadsThreadsSearch
@auth.on.threads.create_runCreating or updating a runRunsCreate
Assistants@auth.on.assistants.createAssistant creationAssistantsCreate
@auth.on.assistants.readAssistant retrievalAssistantsRead
@auth.on.assistants.updateAssistant updatesAssistantsUpdate
@auth.on.assistants.deleteAssistant deletionAssistantsDelete
@auth.on.assistants.searchListing assistantsAssistantsSearch
Crons@auth.on.crons.createCron job creationCronsCreate
@auth.on.crons.readCron job retrievalCronsRead
@auth.on.crons.updateCron job updatesCronsUpdate
@auth.on.crons.deleteCron job deletionCronsDelete
@auth.on.crons.searchListing cron jobsCronsSearch
“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.

Next steps

For implementation details:
Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.