Skip to content

Commit bcc2138

Browse files
rickyromboclaude
andcommitted
Return 401 for expired OAuth access tokens
When an OAuth access token expired, requests asserting a caller identity (via ?user_id= or :wallet) returned 403 because the bearer token failed to resolve to a wallet and the authorization check ran with an empty wallet. 403 implies the caller is authenticated but unauthorized, which prevents clients from realizing they need to refresh their token. Return 401 when a bearer token was supplied but no auth path could resolve it, so clients can refresh and retry. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c5647da commit bcc2138

File tree

2 files changed

+58
-7
lines changed

2 files changed

+58
-7
lines changed

api/auth_middleware.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
255255
// 4. Signature headers - legacy method used for reads
256256
var wallet string
257257

258+
// Detect a supplied Bearer token up front so that, if no auth path can
259+
// resolve a wallet from it, we can return 401 (auth attempted but invalid)
260+
// instead of 403 (auth succeeded but unauthorized).
261+
var bearerToken string
262+
if authHeader := c.Get("Authorization"); authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
263+
bearerToken = strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
264+
}
265+
258266
// Start by trying to get the API key/secret from the Authorization header
259267
signer, _ := app.getApiSigner(c)
260268
myId := app.getMyId(c)
@@ -264,12 +272,6 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
264272
} else {
265273
// The api secret couldn't be found, try other methods:
266274

267-
// Extract Bearer token once for the fallback checks below
268-
var bearerToken string
269-
if authHeader := c.Get("Authorization"); authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
270-
bearerToken = strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
271-
}
272-
273275
if bearerToken != "" {
274276
// OAuth JWT fallback: when Bearer token is not api_access_key, try as OAuth JWT (Plans app)
275277
if wallet == "" && myId != 0 {
@@ -324,6 +326,15 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
324326
// A valid PKCE access token already proves the user authorized this client
325327
_, pkceAuthed := c.Locals("oauthScope").(string)
326328

329+
myWallet := c.Params("wallet")
330+
331+
// A Bearer token was provided but no auth path could resolve it (expired,
332+
// revoked, or otherwise invalid). Return 401 so clients know to refresh
333+
// rather than 403, which implies an authorization (not authentication) failure.
334+
if wallet == "" && bearerToken != "" && (myId != 0 || myWallet != "") {
335+
return fiber.NewError(fiber.StatusUnauthorized, "Invalid or expired access token")
336+
}
337+
327338
// Not authorized to act on behalf of myId
328339
if myId != 0 && !pkceAuthed && !app.isAuthorizedRequest(c.Context(), myId, wallet) {
329340
return fiber.NewError(
@@ -337,7 +348,6 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
337348
}
338349

339350
// Not authorized to act on behalf of myWallet
340-
myWallet := c.Params("wallet")
341351
if myWallet != "" && !strings.EqualFold(myWallet, wallet) {
342352
return fiber.NewError(
343353
fiber.StatusForbidden,

api/auth_middleware_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,47 @@ func TestRequireAuthMiddleware(t *testing.T) {
128128
assert.Equal(t, fiber.StatusUnauthorized, res.StatusCode)
129129
}
130130

131+
// An invalid/expired Bearer token used against an endpoint that asserts a
132+
// caller identity (myId via ?user_id, or :wallet route param) must return
133+
// 401 — the credential was supplied but couldn't be validated. Returning 403
134+
// here would imply the caller is authenticated but unauthorized, which would
135+
// keep clients from realizing they need to refresh their token.
136+
func TestAuthMiddlewareInvalidBearerReturns401(t *testing.T) {
137+
app := testAppWithFixtures(t)
138+
139+
testApp := fiber.New()
140+
testApp.Get("/", app.resolveMyIdMiddleware, app.authMiddleware, func(c *fiber.Ctx) error {
141+
return c.SendStatus(fiber.StatusOK)
142+
})
143+
testApp.Get("/account/:wallet", app.resolveMyIdMiddleware, app.authMiddleware, func(c *fiber.Ctx) error {
144+
return c.SendStatus(fiber.StatusOK)
145+
})
146+
147+
t.Run("invalid bearer with myId returns 401", func(t *testing.T) {
148+
req := httptest.NewRequest("GET", "/?user_id=7eP5n", nil)
149+
req.Header.Set("Authorization", "Bearer expired-or-invalid-token")
150+
res, err := testApp.Test(req, -1)
151+
assert.NoError(t, err)
152+
assert.Equal(t, fiber.StatusUnauthorized, res.StatusCode)
153+
})
154+
155+
t.Run("invalid bearer with wallet param returns 401", func(t *testing.T) {
156+
req := httptest.NewRequest("GET", "/account/0x111c616ae836ceca1effe00bd07f2fdbf9a082bc", nil)
157+
req.Header.Set("Authorization", "Bearer expired-or-invalid-token")
158+
res, err := testApp.Test(req, -1)
159+
assert.NoError(t, err)
160+
assert.Equal(t, fiber.StatusUnauthorized, res.StatusCode)
161+
})
162+
163+
t.Run("invalid bearer without myId or wallet passes through", func(t *testing.T) {
164+
req := httptest.NewRequest("GET", "/", nil)
165+
req.Header.Set("Authorization", "Bearer expired-or-invalid-token")
166+
res, err := testApp.Test(req, -1)
167+
assert.NoError(t, err)
168+
assert.Equal(t, fiber.StatusOK, res.StatusCode)
169+
})
170+
}
171+
131172
func TestWalletCache(t *testing.T) {
132173
app := emptyTestApp(t)
133174

0 commit comments

Comments
 (0)