fix(devices): /api/devices/pair accepts sk- bearer (TokenOrUserAuth)
Real test in C:\temp\v2_probe.js shows POST /api/devices/pair returning
HTTP 200 with body {"success":false, "message":"Unauthorized, invalid
access token"} when called with a Bearer sk- — the same sk- the OAuth
callback hands the client. Root cause: the route group used UserAuth(),
which only accepts a session cookie or a user JWT in Authorization, not
a relay-tier sk- bearer.
The OAuth-redirect flow (Heicode default) never produces a JWT — it
just hands cc-haha a sk-. So in production the pair call after
"一键登录" always 401'd, device-binding never activated, and V2
encryptedFetch silently fell back to legacy bearer for every request.
Fix: split the /devices route into two groups.
- /devices/* (list, rename, revoke): still UserAuth(). A sk- must
NOT be allowed to enumerate or revoke another device — that
would let an attacker with a stolen sk- delete the legitimate
owner's device binding.
- /devices/pair: TokenOrUserAuth(). Pair is the bootstrap step, by
definition no device key exists yet, so sk- IS the only credential
available on the OAuth-redirect flow.
TokenOrUserAuth calls c.Set("id", token.UserId) via its TokenAuth
fallback, so the PairDevice controller's c.GetInt("id") keeps working.
Verified by re-running v2_probe.js after deploy: pair returns
HTTP 200 success:true.
This commit is contained in:
@@ -326,13 +326,28 @@ func SetApiRouter(router *gin.Engine) {
|
||||
// model, and the device-pair flow doesn't accidentally show
|
||||
// up in the legacy /api/token UI. CriticalRateLimit on the
|
||||
// pair endpoint stops brute-force device-spam from one user.
|
||||
deviceRoute := apiRouter.Group("/devices")
|
||||
deviceRoute.Use(middleware.UserAuth())
|
||||
// V2 first-launch pairing accepts EITHER a user JWT (credentials
|
||||
// login flow) OR a sk- access token (OAuth-redirect flow). The
|
||||
// OAuth callback hands cc-haha only a sk-, so the original
|
||||
// UserAuth-only path made pair 401 and the device-binding flow
|
||||
// failed silently. We split /pair onto its own group with
|
||||
// TokenOrUserAuth, while keeping the read/manage routes behind
|
||||
// UserAuth (a sk- bearer must NOT be allowed to enumerate or
|
||||
// revoke the user's other devices).
|
||||
deviceManageRoute := apiRouter.Group("/devices")
|
||||
deviceManageRoute.Use(middleware.UserAuth())
|
||||
{
|
||||
deviceRoute.GET("/", controller.ListUserDevices)
|
||||
deviceRoute.POST("/pair", middleware.CriticalRateLimit(), controller.PairDevice)
|
||||
deviceRoute.PATCH("/:id", controller.RenameUserDevice)
|
||||
deviceRoute.DELETE("/:id", controller.RevokeUserDevice)
|
||||
deviceManageRoute.GET("/", controller.ListUserDevices)
|
||||
deviceManageRoute.PATCH("/:id", controller.RenameUserDevice)
|
||||
deviceManageRoute.DELETE("/:id", controller.RevokeUserDevice)
|
||||
}
|
||||
devicePairRoute := apiRouter.Group("/devices")
|
||||
devicePairRoute.Use(middleware.TokenOrUserAuth())
|
||||
{
|
||||
devicePairRoute.POST("/pair",
|
||||
middleware.CriticalRateLimit(),
|
||||
controller.PairDevice,
|
||||
)
|
||||
}
|
||||
|
||||
usageRoute := apiRouter.Group("/usage")
|
||||
|
||||
Reference in New Issue
Block a user