Skip to content

Commit 1d3c9f4

Browse files
[API-75] Implement full requireAuthMiddleware (#51)
Further implements requireAuthMiddleware to return appropriate 401 and 403 responses (including manager mode support)
1 parent 7965f84 commit 1d3c9f4

11 files changed

Lines changed: 142 additions & 6 deletions

api/auth_middleware.go

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,35 @@ func (app *ApiServer) recoverAuthorityFromSignatureHeaders(c *fiber.Ctx) (int32,
6161
return userId, walletLower
6262
}
6363

64+
// Checks if authedWallet is authorized to act on behalf of userId
65+
func (app *ApiServer) isAuthorizedRequest(c *fiber.Ctx, userId int32, authedWallet string) bool {
66+
cacheKey := fmt.Sprintf("%d:%s", userId, authedWallet)
67+
if hit, ok := app.resolveGrantCache.Get(cacheKey); ok {
68+
return hit
69+
}
70+
71+
var isAuthorized bool
72+
err := app.pool.QueryRow(c.Context(), `
73+
SELECT EXISTS (
74+
SELECT 1
75+
FROM grants
76+
WHERE
77+
is_current = true
78+
AND user_id = $1
79+
AND grantee_address = $2
80+
AND is_approved = true
81+
AND is_revoked = false
82+
)
83+
`, userId, authedWallet).Scan(&isAuthorized)
84+
85+
if err != nil {
86+
return false
87+
}
88+
89+
app.resolveGrantCache.Set(cacheKey, isAuthorized)
90+
return isAuthorized
91+
}
92+
6493
func (app *ApiServer) getAuthedUserId(c *fiber.Ctx) int32 {
6594
return int32(c.Locals("authedUserId").(int32))
6695
}
@@ -78,11 +107,28 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
78107
return c.Next()
79108
}
80109

81-
// Middleware that asserts authedUserId is valid
82-
func (app *ApiServer) requiresAuthMiddleware(c *fiber.Ctx) error {
110+
// Middleware that asserts the authedUserId is valid and is the same as the userId in
111+
// the request path or a managed user of the authedUserId
112+
func (app *ApiServer) requireAuthMiddleware(c *fiber.Ctx) error {
83113
authedUserId := app.getAuthedUserId(c)
114+
authedWallet := app.getAuthedWallet(c)
115+
myId := app.getMyId(c)
116+
wallet := c.Params("wallet")
84117
if authedUserId == 0 {
85118
return fiber.NewError(fiber.StatusUnauthorized, "You must be logged in to make this request")
86119
}
87-
return c.Next()
120+
121+
if myId != 0 && myId == authedUserId {
122+
return c.Next()
123+
}
124+
125+
if wallet != "" && wallet == authedWallet {
126+
return c.Next()
127+
}
128+
129+
if app.isAuthorizedRequest(c, myId, authedWallet) {
130+
return c.Next()
131+
}
132+
133+
return fiber.NewError(fiber.StatusForbidden, "You are not authorized to make this request")
88134
}

api/auth_middleware_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,53 @@ func TestRecoverAuthorityFromSignatureHeaders(t *testing.T) {
2828
assert.Equal(t, int32(1), userId)
2929
assert.Equal(t, "0x7d273271690538cf855e5b3002a0dd8c154bb060", wallet)
3030
}
31+
32+
func TestRequireAuthMiddleware(t *testing.T) {
33+
// Create a dummy endpoint to test the requireAuthMiddleware
34+
testApp := fiber.New()
35+
testApp.Get("/", app.resolveMyIdMiddleware, app.authMiddleware, app.requireAuthMiddleware, func(c *fiber.Ctx) error {
36+
return c.SendStatus(fiber.StatusOK)
37+
})
38+
39+
// Unauthorized when no auth headers
40+
req1 := httptest.NewRequest("GET", "/", nil)
41+
res, err := testApp.Test(req1, -1)
42+
assert.NoError(t, err)
43+
assert.Equal(t, fiber.StatusUnauthorized, res.StatusCode)
44+
45+
// Forbidden when not authorized
46+
req2 := httptest.NewRequest("GET", "/?user_id=1", nil)
47+
// wallet: 0x681c616ae836ceca1effe00bd07f2fdbf9a082bc
48+
req2.Header.Set("Encoded-Data-Message", "signature:1745543704165")
49+
req2.Header.Set("Encoded-Data-Signature", "0x4af765948dccd72026f1059a59c7a6a1172628255d7d387d1590c0fe43961c5908fc6011443805ca0dbd39156300c04dc21bbfa9adce50acea9ad29a7e2fde2a1b")
50+
res, err = testApp.Test(req2, -1)
51+
assert.NoError(t, err)
52+
assert.Equal(t, fiber.StatusForbidden, res.StatusCode)
53+
54+
// Forbidden when grant is revoked
55+
req3 := httptest.NewRequest("GET", "/?user_id=1", nil)
56+
// wallet: 0xc451c1f8943b575158310552b41230c61844a1c1
57+
req3.Header.Set("Encoded-Data-Message", "signature:1745542789211")
58+
req3.Header.Set("Encoded-Data-Signature", "0xffd5f92c0d253c7222cd407cf3398fac664530ef968bd4435ea698ba1daee1d73353330848b65d212eeeaae9f41e177e49078c4efa1131e5e517090626f6dd961c")
59+
res, err = testApp.Test(req3, -1)
60+
assert.NoError(t, err)
61+
assert.Equal(t, fiber.StatusForbidden, res.StatusCode)
62+
63+
// Authorized when grant is approved
64+
req4 := httptest.NewRequest("GET", "/?user_id=1", nil)
65+
// wallet: 0x5f1a372b28956c8363f8bc3a231a6e9e1186ead8
66+
req4.Header.Set("Encoded-Data-Message", "signature:1745544459796")
67+
req4.Header.Set("Encoded-Data-Signature", "0x1c9cb405d8437d28ff5596918551f7a45f981e81618d65ee10892313292a8c7a325af002231d115b28ca2d244b082abe1bde4a7d9610f8140d3738a9be5c4fd91b")
68+
res, err = testApp.Test(req4, -1)
69+
assert.NoError(t, err)
70+
assert.Equal(t, fiber.StatusOK, res.StatusCode)
71+
72+
// Authorized when own user
73+
req5 := httptest.NewRequest("GET", "/?user_id=1", nil)
74+
// wallet: 0x7d273271690538cf855e5b3002a0dd8c154bb060
75+
req5.Header.Set("Encoded-Data-Message", "signature:1744763856446")
76+
req5.Header.Set("Encoded-Data-Signature", "0xbb202be3a7f3a0aa22c1458ef6a3f2f8360fb86791c7b137e8562df0707825c11fa1db01096efd2abc5e6613c4d1e8d4ae1e2b993abdd555fe270c1b17bff0d21c")
77+
res, err = testApp.Test(req5, -1)
78+
assert.NoError(t, err)
79+
assert.Equal(t, fiber.StatusOK, res.StatusCode)
80+
}

api/fixture_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,19 @@ var (
148148
"splits": "[]",
149149
"created_at": time.Now(),
150150
}
151+
152+
grantBaseRow = map[string]any{
153+
"blockhash": "block1",
154+
"blocknumber": 101,
155+
"user_id": nil,
156+
"is_current": true,
157+
"grantee_address": nil,
158+
"is_approved": false,
159+
"is_revoked": false,
160+
"created_at": time.Now(),
161+
"updated_at": time.Now(),
162+
"txhash": "tx123",
163+
}
151164
)
152165

153166
func insertFixtures(table string, baseRow map[string]any, csvFile string) {

api/resolve_middleware.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ func (app *ApiServer) resolveMyIdMiddleware(c *fiber.Ctx) error {
2222
}
2323

2424
func (app *ApiServer) getMyId(c *fiber.Ctx) int32 {
25-
return int32(c.Locals("myId").(int))
25+
myId := c.Locals("myId")
26+
if myId == nil {
27+
return 0
28+
}
29+
return int32(myId.(int))
2630
}
2731

2832
func (app *ApiServer) requireUserIdMiddleware(c *fiber.Ctx) error {

api/server.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ func NewApiServer(config config.Config) *ApiServer {
8282
}
8383

8484
pool, err := pgxpool.NewWithConfig(context.Background(), connConfig)
85+
// To turn off pgx logging, use this:
86+
// pool, err := pgxpool.New(context.Background(), config.DbUrl)
87+
8588
if err != nil {
8689
logger.Fatal("db connect failed", zap.Error(err))
8790
}
@@ -100,6 +103,14 @@ func NewApiServer(config config.Config) *ApiServer {
100103
panic(err)
101104
}
102105

106+
resolveGrantCache, err := otter.MustBuilder[string, bool](50_000).
107+
WithTTL(10 * time.Minute).
108+
CollectStats().
109+
Build()
110+
if err != nil {
111+
panic(err)
112+
}
113+
103114
privateKey, err := crypto.HexToECDSA(config.DelegatePrivateKey)
104115
if err != nil {
105116
panic(err)
@@ -117,6 +128,7 @@ func NewApiServer(config config.Config) *ApiServer {
117128
started: time.Now(),
118129
resolveHandleCache: resolveHandleCache,
119130
resolveWalletCache: resolveWalletCache,
131+
resolveGrantCache: resolveGrantCache,
120132
rewardAttester: *rewards.NewRewardAttester(privateKey, []rewards.Reward{}),
121133
solanaConfig: config.SolanaConfig,
122134
antiAbuseOracles: config.AntiAbuseOracles,
@@ -183,7 +195,7 @@ func NewApiServer(config config.Config) *ApiServer {
183195
// Users
184196
g.Get("/users", app.v1Users)
185197

186-
g.Get("/users/account/:wallet", app.requiresAuthMiddleware, app.v1UsersAccount)
198+
g.Get("/users/account/:wallet", app.requireAuthMiddleware, app.v1UsersAccount)
187199

188200
g.Use("/users/handle/:handle", app.requireHandleMiddleware)
189201
g.Get("/users/handle/:handle", app.v1User)
@@ -255,6 +267,7 @@ type ApiServer struct {
255267
started time.Time
256268
resolveHandleCache otter.Cache[string, int32]
257269
resolveWalletCache otter.Cache[string, int32]
270+
resolveGrantCache otter.Cache[string, bool]
258271
rewardAttester rewards.RewardAttester
259272
solanaConfig config.SolanaConfig
260273
antiAbuseOracles []string

api/server_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func TestMain(m *testing.M) {
7878
insertFixtures("aggregate_user_tips", aggregateUserTipsBaseRow, "testdata/aggregate_user_tips_fixtures.csv")
7979
insertFixtures("usdc_purchases", usdcPurchaseBaseRow, "testdata/usdc_purchases_fixtures.csv")
8080
insertFixtures("track_routes", map[string]any{}, "testdata/track_routes_fixtures.csv")
81+
insertFixtures("grants", grantBaseRow, "testdata/grants_fixtures.csv")
8182

8283
// index to es / os
8384

api/testdata/grants_fixtures.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
user_id,grantee_address,is_approved,is_revoked
2+
1,0x5f1a372b28956c8363f8bc3a231a6e9e1186ead8,true,false
3+
1,0xc451c1f8943b575158310552b41230c61844a1c1,false,true

api/testdata/user_fixtures.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ user_id,handle,handle_lc,is_deactivated,wallet,playlist_library
66
5,guyintrending,guyintrending,f,0x34567890abcdef13,
77
6,TracksByPermalink,tracksbypermalink,f,0xffffffffff,
88
91,badguy,badguy,t,0x4567890abcdef123,
9+
100,authtest1,authtest1,f,0x681c616ae836ceca1effe00bd07f2fdbf9a082bc,
10+
101,authtest2,authtest2,f,0xc451c1f8943b575158310552b41230c61844a1c1,
11+
102,authtest3,authtest3,f,0x5f1a372b28956c8363f8bc3a231a6e9e1186ead8,

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ require (
4949
github.com/google/go-querystring v1.1.0 // indirect
5050
github.com/google/uuid v1.6.0 // indirect
5151
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
52+
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
5253
github.com/holiman/uint256 v1.3.2 // indirect
5354
github.com/jackc/pgpassfile v1.0.0 // indirect
5455
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
7272
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
7373
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
7474
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
75+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
76+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
7577
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
7678
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
7779
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=

0 commit comments

Comments
 (0)