Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions api/auth_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,25 +255,36 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
// 4. Signature headers - legacy method used for reads
var wallet string

// Detect a supplied Bearer token up front so that, if no auth path can
// validate it, we can return 401 (auth attempted but invalid) instead of
// 403 (auth succeeded but unauthorized). bearerValidated tracks whether
// any path successfully decoded the token — a valid-but-mismatched token
// is still 403, only an undecodable/expired/revoked token is 401.
var bearerToken string
var bearerValidated bool
if authHeader := c.Get("Authorization"); authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
bearerToken = strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
}

// Start by trying to get the API key/secret from the Authorization header
signer, _ := app.getApiSigner(c)
myId := app.getMyId(c)
if signer != nil {
app.logger.Debug("authMiddleware: resolved via app bearer/secret/oauth", zap.String("wallet", strings.ToLower(signer.Address)))
wallet = strings.ToLower(signer.Address)
// signer != nil via Bearer means getApiSigner validated the credential
// (api_access_key, OAuth token + api_secret, or AudiusApiSecret path).
if bearerToken != "" {
bearerValidated = true
}
} else {
// The api secret couldn't be found, try other methods:

// Extract Bearer token once for the fallback checks below
var bearerToken string
if authHeader := c.Get("Authorization"); authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
bearerToken = strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
}

if bearerToken != "" {
// OAuth JWT fallback: when Bearer token is not api_access_key, try as OAuth JWT (Plans app)
if wallet == "" && myId != 0 {
if oauthWallet, jwtUserId, err := app.validateOAuthJWTTokenToWalletAndUserId(c.Context(), bearerToken); err == nil {
bearerValidated = true
if int32(jwtUserId) == myId {
app.logger.Debug("authMiddleware: resolved via OAuth JWT", zap.String("wallet", oauthWallet), zap.Int32("myId", myId))
wallet = oauthWallet
Expand All @@ -288,6 +299,7 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
// PKCE token fallback: resolve opaque Bearer token from oauth_tokens in case the getSigner fails because there's no secret stored in the api_keys table
if wallet == "" {
if entry, ok := app.lookupOAuthAccessToken(c, bearerToken); ok {
bearerValidated = true
if myId == 0 || entry.UserID == myId {
wallet = strings.ToLower(entry.ClientID)
c.Locals("oauthScope", entry.Scope)
Expand Down Expand Up @@ -324,6 +336,17 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
// A valid PKCE access token already proves the user authorized this client
_, pkceAuthed := c.Locals("oauthScope").(string)

myWallet := c.Params("wallet")

// A Bearer token was provided but no auth path could validate it (expired,
// revoked, or otherwise undecodable). Return 401 so clients know to refresh
// rather than 403, which implies an authorization (not authentication) failure.
// A token that decoded successfully but didn't match the requested user is
// still 403 below — it's a permission issue, not a credential issue.
if wallet == "" && bearerToken != "" && !bearerValidated && (myId != 0 || myWallet != "") {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid or expired access token")
}

// Not authorized to act on behalf of myId
if myId != 0 && !pkceAuthed && !app.isAuthorizedRequest(c.Context(), myId, wallet) {
return fiber.NewError(
Expand All @@ -337,7 +360,6 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
}

// Not authorized to act on behalf of myWallet
myWallet := c.Params("wallet")
if myWallet != "" && !strings.EqualFold(myWallet, wallet) {
return fiber.NewError(
fiber.StatusForbidden,
Expand Down
41 changes: 41 additions & 0 deletions api/auth_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,47 @@ func TestRequireAuthMiddleware(t *testing.T) {
assert.Equal(t, fiber.StatusUnauthorized, res.StatusCode)
}

// An invalid/expired Bearer token used against an endpoint that asserts a
// caller identity (myId via ?user_id, or :wallet route param) must return
// 401 — the credential was supplied but couldn't be validated. Returning 403
// here would imply the caller is authenticated but unauthorized, which would
// keep clients from realizing they need to refresh their token.
func TestAuthMiddlewareInvalidBearerReturns401(t *testing.T) {
app := testAppWithFixtures(t)

testApp := fiber.New()
testApp.Get("/", app.resolveMyIdMiddleware, app.authMiddleware, func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})
testApp.Get("/account/:wallet", app.resolveMyIdMiddleware, app.authMiddleware, func(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
})

t.Run("invalid bearer with myId returns 401", func(t *testing.T) {
req := httptest.NewRequest("GET", "/?user_id=7eP5n", nil)
req.Header.Set("Authorization", "Bearer expired-or-invalid-token")
res, err := testApp.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, fiber.StatusUnauthorized, res.StatusCode)
})

t.Run("invalid bearer with wallet param returns 401", func(t *testing.T) {
req := httptest.NewRequest("GET", "/account/0x111c616ae836ceca1effe00bd07f2fdbf9a082bc", nil)
req.Header.Set("Authorization", "Bearer expired-or-invalid-token")
res, err := testApp.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, fiber.StatusUnauthorized, res.StatusCode)
})

t.Run("invalid bearer without myId or wallet passes through", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer expired-or-invalid-token")
res, err := testApp.Test(req, -1)
assert.NoError(t, err)
assert.Equal(t, fiber.StatusOK, res.StatusCode)
})
}

func TestWalletCache(t *testing.T) {
app := emptyTestApp(t)

Expand Down
Loading