通过 OAuth 2.0 让 ChatGPT 以真实用户身份调用 Laravel API,实现用户说一句话即可查询绑定的社交渠道等业务操作。
如果你的用户只需要对 ChatGPT 说话就能操作你的 Laravel 应用,那会怎样?
不是把 ChatGPT 里的信息复制粘贴到你的应用中。
也不是在你的网站里嵌入另一个聊天机器人。
我说的是让 ChatGPT 安全地以某个用户身份认证,并对你的 Laravel API 执行真实的操作。
我最近在为一个生产环境的 Laravel 应用构建 GPT Action 时,刚好实现了这种集成。
最终架构允许用户说一句简单的话,比如:
Show me my connected social channels.
ChatGPT 通过 OAuth 认证,调用 Laravel API,将 OAuth 身份解析为正确的应用用户,验证请求的 scope,然后返回该用户的真实数据。
架构大致如下:
ChatGPT
↓
Custom GPT Action
↓
OAuth 2.0
↓
Laravel Authentication Application
↓
Laravel Passport
↓
Account Mapping
↓
Main Laravel Application
↓
Application API
↓
User's Data
然而,让所有这些组件协同工作,暴露了一些在你开始实验 GPT Actions 时并非一目了然的实现细节。
在本文中,我将逐步介绍架构、OAuth 流程、OpenAPI schema、Laravel Passport 配置、账户关联、scope、调试、部署问题,以及在计划公开分发 GPT 之前你应该了解的 一个非常重要的 ChatGPT 账户限制。
如果你读这篇文章是想为现有的 Laravel 应用添加 ChatGPT Actions、AI 集成、OAuth、API 或其他自定义功能,那正是我们在 Custom PHP Design 做的事情。
👉 Custom Laravel and PHP Development:
自定义 GPT 能做的远不止生成文本。
GPT Actions 允许 GPT 与外部 API 通信。
这意味着 GPT 可能会:
检索客户账户信息
查询应用数据
与内部业务系统交互
触发 Laravel 任务和服务
与现有 SaaS 功能配合使用
GPT 通过 OpenAPI 规范来了解你的应用能做什么。
openapi: 3.1.0
info:
title: "Example Application API"
version: 1.0.0
servers:
- url: https://example.com
paths:
/api/v1/channels:
get:
operationId: getConnectedChannels
summary: Get the authenticated user's connected channels
responses:
'200':
description: "Connected channels"
一旦导入 GPT Action 配置,ChatGPT 就知道它有一个名为:
getConnectedChannels
的操作。
当用户问类似这样的问题时,模型可以决定调用该操作:
What accounts do I have connected?
这为现有 Laravel 应用创造了一个非常有趣的接口。
与其强迫每个工作流都通过表单、仪表盘和导航菜单来完成,有些工作流可以变成对话式的。
这并不意味着用 ChatGPT 取代你的应用界面。
它的意思是给你的客户提供另一种方式来与你已经构建的功能进行交互。
如果你已经有一个成熟的 Laravel 应用,并想探索这种类型的集成,请访问:
对于公开的应用数据,GPT Action 可能在不需要用户认证的情况下运行。
但大多数有用的 SaaS 应用都包含用户特定的数据。
假设两个客户使用你的 GPT。
客户 A 应该看到:
Customer A's data
客户 B 应该看到:
Customer B's data
客户 A 绝不能访问客户 B 的信息。
因此 GPT 需要独立认证每个用户。
这就是 OAuth 的用武之地。
一个典型的 OAuth Action 需要:
Client ID
Client Secret
Authorization URL
Token URL
Scopes
对于 Laravel 来说,Laravel Passport 是一个自然的选择,因为它提供了完整的 OAuth2 服务器实现。
我们的配置最终在概念上类似于:
Authorization URL:
https://auth.example.com/oauth/authorize
Token URL:
https://auth.example.com/oauth/token
GPT 收到自己的 OAuth 客户端:
Client ID
Client Secret
Redirect URI
Grant Types
重要的 grant types 是:
authorization_code
refresh_token
刷新令牌尤其重要,因为你不希望客户每次 ChatGPT 需要调用你的 API 时都要重新认证。
这个实现还有一个额外的架构挑战。
主应用和 OAuth 服务器是两个独立的 Laravel 代码库。
main-application
auth-application
主应用包含:
应用权限
现有 API 认证
Auth 应用包含:
外部身份映射
你可能会因为以下几种原因最终使用这种架构。
你可能希望将认证与主应用隔离。
你可能已经有一个独立的认证服务。
你可能在改造一个遗留 Laravel 应用。
你可能希望多个应用最终使用同一个 OAuth provider。
不管原因是什么,分离应用会产生一个重要的问题:
Auth 应用中的 OAuth 用户如何映射回主 Laravel 应用中的正确用户?
这成为这次集成中最重要的部分之一。
想象一下这种情况。
你的主要 Laravel 应用包含:
Main Laravel Application
users
----------------
id = 123
你的 OAuth 应用有自己的 users 表:
Auth Laravel Application
users
----------------
id = 456
ChatGPT 收到一个与以下用户关联的 OAuth 访问令牌:
Auth User 456
但你的主应用的 API 需要知道:
Main Application User 123
这两个身份并非天生相同。
你需要它们之间的可信映射。
Auth User 456
↓
account_links
↓
Application User 123
一个关联表可以表示这种关系:
application_account_links
auth_user_id
application_user_id
然后 OAuth 令牌 introspection 可以返回概念上类似于:
{
"active": true,
"linked": true,
"application_user_id": 123,
"scopes": [
"profile",
"channels.read",
"posts.read",
"posts.write"
]
}
主 Laravel 应用现在可以将传入的 API 请求认证为用户 123。
这就是以下两者之间的桥梁:
ChatGPT OAuth identity
你的真实 Laravel 客户
这部分很关键。
你不应该像这样构建账户关联:
/connect?user_id=123
然后盲目信任那个 ID。
攻击者可以简单地把它改成:
/connect?user_id=124
相反,主 Laravel 应用应该以加密方式断言身份。
我们通过一个短期签名移交来实现这一点。
主应用生成一个包含如下声明的短期签名令牌:
{
"iss": "example.com",
"sub": "123",
"aud": "auth.example.com",
"iat": 1787540000,
"exp": 1787540300,
"jti": "unique-random-value"
}
重要的声明是:
sub
这代表主应用中的规范用户 ID。
令牌使用 RSA SHA-256 签名:
RS256
架构变为:
Main Laravel Application
Private Key
↓
Sign handoff token
↓
Auth Laravel Application
Public Key
↓
Verify signature
私钥保留在生成身份断言的应用中。
Auth 应用只需要公钥来验证它。
这给了 Auth 应用加密证明:
主应用说这个请求属于用户 123。
这比信任浏览器提供的标识符要安全得多。
验证 RSA 签名是必要的。
Auth 应用应该验证如下声明:
alg
kid
iss
aud
iat
exp
sub
jti
alg = RS256
issuer = expected application
audience = expected Auth service
expiration = still valid
subject = valid canonical user ID
jti = unique
alg = none
不要静默接受意外的算法。
不要接受过期的移交。
在签名验证完成之前不要信任 subject。
移交的目的是在两个 Laravel 应用之间创建一个小的、明确的信任边界。
一个有效的签名移交通常只能兑换一次。
这就是我们包含:
jti
的原因。
Auth 应用记录每个已兑换的 JTI。
handoff_redemptions
id
jti
application_user_id
redeemed_by_auth_user_id
redeemed_at
数据库应该在以下字段上强制唯一性:
jti
现在,如果有人试图重用完全相同的移交,Auth 可以拒绝它。
即使有人以某种方式获得了以前有效的移交 URL,这也能提供重放保护。
我们最初的实现暴露了一个有趣的 UX 问题。
Auth 应用表现得像一个正常的独立 Laravel 应用。
一个没有 Auth session 的用户到达时会看到:
Login
从技术上讲,这很合理。
从产品角度来看,这样做并不合理。
客户已经在主应用中有账户了。
为什么要仅仅因为我们内部决定使用单独的 OAuth 服务,就让他们再创建一个账户和密码呢?
更好的体验是:
用户登录主 Laravel 应用
↓
连接 ChatGPT
↓
主应用创建签名交接
↓
Auth 验证交接
↓
Auth 解析或配置内部身份
↓
创建账户映射
↓
OAuth 授权继续
Auth 用户成为一个实现细节。
客户不需要知道它的存在。
这是构建身份验证系统时的一个重要经验。
技术上的正确身份验证和良好的身份验证用户体验之间是有区别的。
如果你的 OAuth 服务器需要自己的本地用户记录,你可以在验证签名交接后自动配置该内部身份。
$link = AccountLink::where(
'application_user_id',
$verifiedSubject
)->first();
if ($link) {
$authUser = $link->authUser;
} else {
$authUser = createInternalAuthUser();
AccountLink::create([
'auth_user_id' => $authUser->id,
'application_user_id' => $verifiedSubject,
]);
}
实际实现应该是事务性的并强制唯一性约束。
重要的理念是用户不需要手动注册你的 OAuth 服务。
他们的身份已经由可信的主应用建立了。
不要给 GPT 无限的 API 访问权限。
profile
channels.read
posts.read
posts.write
你的 Laravel API 路由可以要求特定的作用域。
Route::get('/connected-channels', ...)
->middleware('external-scope:channels.read');
发布端点可能需要:
posts.write
读取现有帖子可能需要:
posts.read
这给你一个清晰的权限边界。
如果你以后引入:
analytics.read
billing.read
account.write
这些能力可以保持不可用,除非被明确授权。
这对于 AI 集成尤其重要,因为你应该只暴露 AI 客户端实际需要的 capabilities。
我们的主应用已经有自己的 API 身份验证。
我们不希望 ChatGPT OAuth 支持破坏或替换现有的 API 客户端。
相反,API 中间件可以支持多种身份验证路径。
传入的 API 请求
↓
现有的 API 密钥?
↓ 是
正常身份验证
↓ 否
外部 OAuth Bearer 令牌?
↓
请求 Auth 应用审查令牌
↓
解析应用用户
↓
应用 OAuth 作用域
↓
继续请求
这允许现有集成继续工作,而 ChatGPT 使用 OAuth。
这是一个重要的架构原则:
在不必要地重写现有应用基础设施的情况下,添加 AI 集成能力。
在 Custom PHP Design,我们处理 Laravel 现代化项目的方法正是如此:与现有可用的部分集成,而不是自动假设整个应用需要重建。
主应用不应直接查询 Passport 的数据库。
这会将两个应用紧密耦合。
主应用
↓
服务器对服务器请求
↓
Auth 应用
↓
Passport 令牌验证
↓
账户映射
↓
审查响应
请求应使用服务器对服务器密钥。
X-Application-Auth-Secret
浏览器永远不会看到这个值。
ChatGPT 永远不会看到这个值。
它严格存在于你的服务器之间。
主应用将 OAuth 访问令牌发送到 Auth 服务进行验证。
{
"token": "oauth-access-token"
}
Auth 应用返回类似以下内容:
{
"active": true,
"linked": true,
"application_user_id": 123,
"scopes": [
"channels.read",
"posts.read",
"posts.write"
]
}
现在你正常的 Laravel 中间件可以解析:
User::findOrFail($applicationUserId);
并继续处理 API 请求。
这值得单独一节。
你的审查密钥永远不应该出现在:
JavaScript
HTML
Blade data attributes
localStorage
sessionStorage
query strings
frontend API calls
通信应该是:
ChatGPT
↓ bearer token
主 Laravel API
↓ 私有服务器对服务器请求
Auth Laravel 应用
浏览器
↓ secret
Auth 应用
保持内部身份验证的内部性。
我们遇到的一个 bug 非常简单。
一个 Laravel 应用发送了类似这样的请求头:
X-Application-Introspection-Secret
而 Auth 应用期望的是类似这样的:
X-Application-Auth-Secret
401 Unauthorized
其他一切都是正确的:
Bearer 令牌存在
账户已关联
API 路由存在
GPT 正在发出请求
但服务器对服务器身份验证在令牌审查发生之前就失败了。
标准化内部身份验证请求头和配置名称。
更好的做法是将服务器对服务器通信封装在服务类中,而不是在你的 Laravel 应用中散落原始 HTTP 请求。
调试 OAuth 时,很容易想到记录所有内容。
access_token
refresh_token
client_secret
private keys
shared secrets
authorization codes
signed handoff tokens
{
"external_introspection_attempted": "yes",
"introspection_http_status": 200,
"introspection_active": "yes",
"introspection_linked": "yes",
"resolved_application_user_id": 123,
"scopes": [
"channels.read"
],
"auth_result": "external_oauth"
}
这告诉了你几乎所有你需要的信息,而不会泄露凭证。
在开发过程中,这非常宝贵。
我们可以观察请求如何流转:
ChatGPT
→ Auth
→ 主 API
→ 审查
→ 用户解析
→ 作用域验证
→ 控制器
当出现问题时,我们确切地知道是哪个边界失败了。
另一个生产环境问题几乎与 OAuth 协议逻辑无关。
Laravel Passport 的 RSA 密钥在部署后具有不正确的文件系统权限。
600
一个 Passport 密钥最终变成了:
644
OAuth 库拒绝了它。
结果在上游表现为:
500 Internal Server Error
修复方法很简单:
sudo chown www-data:www-data storage/oauth-private.key
sudo chown www-data:www-data storage/oauth-public.key
sudo chmod 600 storage/oauth-private.key
sudo chmod 600 storage/oauth-public.key
但手动修复是不够的。
你的部署流程应该强制执行此操作。
echo "Setting Laravel runtime permissions..."
sudo chown -R www-data:www-data storage bootstrap/cache
sudo chmod -R ug+rwX storage bootstrap/cache
if [[ -f storage/oauth-private.key ]]; then
sudo chmod 600 storage/oauth-private.key
fi
if [[ -f storage/oauth-public.key ]]; then
sudo chmod 600 storage/oauth-public.key
fi
这也有助于防止另一个经典的 Laravel 生产环境错误:
storage/logs/laravel.log: Permission denied
生产自动化应该使你的应用在每次部署后都处于已知良好的状态。
后端架构准备就绪后,GPT Action 本身需要 OAuth 配置。
Authentication Type:
OAuth
Client ID:
<passport-client-id>
Client Secret:
<passport-client-secret>
Authorization URL:
https://auth.example.com/oauth/authorize
Token URL:
https://auth.example.com/oauth/token
Scopes:
profile channels.read posts.read posts.write
ChatGPT 提供了一个回调 URL。
该回调必须与你向 OAuth 服务器注册的 redirect URI 匹配。
如果 ChatGPT 给你的是:
https://chat.openai.com/aip/g-example/oauth/callback
那么你的 Passport OAuth 客户端需要该回调。
在开发过程中,回调 URL 可能成为困惑的来源。
如果与 GPT 关联的回调发生变化,你的 Passport 客户端可能仍然包含旧的 redirect URI。
redirect_uri = NEW_CALLBACK
而 Passport 期望的是:
OLD_CALLBACK
为了不必不断创建新的客户端和密钥,我们构建了工具来安全地更新现有 Passport 客户端的 redirect URI,同时保留:
Client ID
Secret
Grant Types
Name
State
这在开发过程中被证明非常有用。
更广泛的教训是:
围绕 OAuth 构建诊断和管理工具,而不是手动编辑数据库行。
另一个经验:不要假设为旧版 Laravel Passport 编写的示例与你的安装匹配。
例如,旧示例可能引用:
redirect
而新版 Passport 版本可以使用如下字段:
redirect_uris
grant_types
如果你对不再包含 redirect 的 schema 运行类似以下内容:
Laravel\Passport\Client::all([
'id',
'name',
'redirect',
'revoked'
]);
你将收到数据库错误。
Always inspect the actual Passport version and schema you're using.
Don't blindly copy an OAuth tutorial written several major versions ago.
One of the best things we did during this project was build small Artisan commands specifically for OAuth diagnostics.
For example, a command could inspect:
Client ID
Client name
Redirect URIs
Grant types
Revoked state
Client type
Secret storage
Exact redirect match
You can even securely prompt for the client secret and verify whether it matches the stored hash without printing either value.
php artisan oauth:inspect-clients \
'CLIENT_ID' \
--redirect-uri='EXACT_CALLBACK' \
--verify-secret
Output might look like:
State: active
Client type: confidential
Secret storage: hashed
Grant types: authorization_code, refresh_token
Exact redirect match: yes
Submitted secret matches: yes
That's dramatically better than guessing.
Authentication gets the GPT through the front door.
Your OpenAPI specification tells it what it can actually do.
Suppose your application exposes:
/api/v1/connected-channels:
get:
operationId: getConnectedChannels
/api/v1/posts:
post:
operationId: createPost
The descriptions matter.
summary: Get the authenticated user's connected social media channels
summary: Get channels
The model needs enough semantic information to understand when an operation applies.
Operation IDs should also be descriptive:
getConnectedChannels
createSocialPost
scheduleSocialPost
getScheduledPosts
get1
post2
action3
Treat your OpenAPI specification as part API contract and part AI interface.
AI-driven API access raises another UX consideration.
Some operations should happen immediately.
Show me my connected channels.
Publish this to Facebook and Instagram.
has an external side effect.
For workflows like publishing, deleting, sending, purchasing, or modifying important records, design your Action and GPT instructions so the user has an opportunity to confirm what will happen.
A good interaction might be:
User:
Create a post about our product launch for Facebook and Instagram.
GPT:
Here's the proposed post:
...
Would you like me to publish this to Facebook and Instagram?
User:
Yes.
GPT:
→ API Action
→ publish
This is better than making every conversational request immediately destructive.
An OAuth token successfully being issued does not mean your integration works.
We tested the entire path:
ChatGPT
↓
Authorization request
↓
Laravel Passport
↓
Authorization code
↓
Token exchange
↓
Access token
↓
GPT Action request
↓
Main Laravel API
↓
External token introspection
↓
Account mapping
↓
Scope validation
↓
Correct application user
↓
Actual application data
The moment that mattered wasn't when Passport issued a token.
It was when we could ask:
Show me my connected social channels.
and receive the channels belonging to the correct application user.
That proved the architecture end-to-end.
Before calling an OAuth integration production-ready, test user isolation.
Create or use at least two application users.
User A
→ OAuth
→ User A's resources
User B
→ OAuth
→ User B's resources
User B
→ OAuth
→ User A's resources
Missing account mapping
Conflicting account mapping
Auth service unavailable
Invalid server-to-server secret
Invalid OAuth client secret
Incorrect redirect URI
Happy-path testing is not enough for authentication systems.
Now for the frustrating part.
You can build the entire integration correctly and still discover that you cannot distribute your GPT the way you expected.
As of 2026, OpenAI's documentation states that new GPT creation and publishing are not available on personal ChatGPT accounts, including:
Free
Go
Plus
Pro
Existing GPTs can remain available to their owners and can still be edited when the applicable plan and permissions allow it.
However, creating and publishing GPTs is currently available through eligible managed workspaces such as:
Business
Enterprise
Edu
subject to workspace permissions.
This is important because upgrading from ChatGPT Plus to the much more expensive personal ChatGPT Pro plan does not necessarily solve the GPT publishing problem.
Pro is still a personal account.
If your goal is to create a GPT that customers can access, you need to evaluate the current Business/Enterprise/Edu workspace requirements before investing significant development time.
This creates an interesting situation for solo developers and small SaaS companies.
Use the GPT privately
Prove the entire architecture
but still need an eligible managed ChatGPT workspace before you can distribute that GPT to customers.
That doesn't make the development work useless.
Your OAuth-enabled API can potentially support many other clients.
And your private GPT can serve as a beta environment while you validate whether customers actually want conversational access to your application.
That's exactly how I would approach it.
Prove demand before adding another recurring expense.
If you already have an existing GPT on a personal account, private testing can be extremely useful.
OAuth
Account linking
API authentication
Scopes
Token refresh
OpenAPI operations
Prompt behavior
Error handling
User confirmation
API response design
before making the integration broadly available.
That gives you a working prototype.
If customers later start asking:
Can I control this from ChatGPT?
you already know the backend architecture works.
At that point, moving the GPT into an eligible managed workspace becomes a business decision rather than an experiment.
One of my biggest takeaways from this project is that GPT Actions aren't primarily about prompt engineering.
The hard part isn't writing:
You are a helpful social media assistant.
The hard part is everything behind it:
Authentication
Authorization
OAuth
Account linking
Scopes
API design
OpenAPI