Skip to content
Open
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
29 changes: 20 additions & 9 deletions api/auth_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,16 +239,27 @@ func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {
wallet = strings.ToLower(signer.Address)
} else {
wallet = app.recoverAuthorityFromSignatureHeaders(c)
// 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 "))
}

// OAuth JWT fallback: when Bearer token is not api_access_key, try as OAuth JWT (Plans app)
if wallet == "" && myId != 0 {
if authHeader := c.Get("Authorization"); authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
if token != "" {
if oauthWallet, jwtUserId, err := app.validateOAuthJWTTokenToWalletAndUserId(c.Context(), token); err == nil {
if int32(jwtUserId) == myId {
wallet = oauthWallet
}
}
if wallet == "" && myId != 0 && bearerToken != "" {
if oauthWallet, jwtUserId, err := app.validateOAuthJWTTokenToWalletAndUserId(c.Context(), bearerToken); err == nil {
if int32(jwtUserId) == myId {
wallet = oauthWallet
}
}
}
// PKCE token fallback: resolve opaque Bearer token from oauth_tokens
if wallet == "" && bearerToken != "" {
if entry, ok := app.lookupOAuthAccessToken(c, bearerToken); ok {
wallet = strings.ToLower(entry.ClientID)
if myId == 0 {
myId = entry.UserID
c.Locals("myId", int(entry.UserID))
}
}
}
Comment on lines +256 to 265
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the PKCE Bearer-token fallback, if myId is already set (via user_id query param), the code does not verify it matches the access token's user_id. That means a valid access token for user A could be used to authorize requests for user B as long as the app wallet has an approved grant for B, effectively turning per-user tokens into per-client tokens. Consider enforcing entry.UserID == myId when myId != 0 (similar to the OAuth JWT fallback), otherwise reject the request.

Copilot uses AI. Check for mistakes.
Expand Down
43 changes: 38 additions & 5 deletions api/request_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,16 @@ func (app *ApiServer) getApiSigner(c *fiber.Ctx) (*Signer, error) {
if token == "" {
return nil, fmt.Errorf("Bearer token is empty")
}
if app.writePool != nil {
if signer := app.getSignerFromApiAccessKey(c.Context(), token); signer != nil {
return signer, nil
}

if signer := app.getSignerFromApiAccessKey(c.Context(), token); signer != nil {
return signer, nil
}

// Try PKCE token → look up client_id → get api_secret from api_keys → return Signer
if signer := app.getSignerFromOAuthToken(c, token); signer != nil {
return signer, nil
}

// If authMiddleware already validated a JWT and set authedWallet,
// use AudiusApiSecret to sign on behalf of the authenticated user.
if wallet, _ := c.Locals("authedWallet").(string); wallet != "" && app.config.AudiusApiSecret != "" {
Expand Down Expand Up @@ -133,7 +138,7 @@ func (app *ApiServer) getSignerFromApiAccessKey(ctx context.Context, apiAccessKe
}

var parentApiKey, apiSecret string
err := app.writePool.QueryRow(ctx, `
err := app.pool.QueryRow(ctx, `
SELECT aak.api_key, ak.api_secret
FROM api_access_keys aak
JOIN api_keys ak ON LOWER(ak.api_key) = LOWER(aak.api_key)
Expand All @@ -158,3 +163,31 @@ func (app *ApiServer) getSignerFromApiAccessKey(ctx context.Context, apiAccessKe
PrivateKey: privateKey,
}
}

// getSignerFromOAuthToken looks up a PKCE access token, resolves the client_id to an api_key,
// then gets the api_secret to build a Signer. This allows writes (ManageEntity signing)
// to work for PKCE-authenticated requests.
func (app *ApiServer) getSignerFromOAuthToken(c *fiber.Ctx, token string) *Signer {
entry, ok := app.lookupOAuthAccessToken(c, token)
if !ok {
return nil
}

// Look up api_secret for the client_id (developer app address = api_key)
var apiSecret string
err := app.pool.QueryRow(c.Context(), `
SELECT api_secret FROM api_keys WHERE LOWER(api_key) = LOWER($1)
`, entry.ClientID).Scan(&apiSecret)
if err != nil || apiSecret == "" {
return nil
}

privateKey, err := crypto.HexToECDSA(strings.TrimPrefix(apiSecret, "0x"))
if err != nil {
return nil
}
return &Signer{
Address: strings.ToLower(entry.ClientID),
PrivateKey: privateKey,
}
}
16 changes: 16 additions & 0 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ func NewApiServer(config config.Config) *ApiServer {
panic(err)
}

oauthTokenCache, err := otter.MustBuilder[string, oauthTokenCacheEntry](10_000).
WithTTL(60 * time.Second).
CollectStats().
Build()
if err != nil {
panic(err)
}

privateKey, err := crypto.HexToECDSA(config.DelegatePrivateKey)
if err != nil {
panic(err)
Expand Down Expand Up @@ -233,6 +241,7 @@ func NewApiServer(config config.Config) *ApiServer {
resolveGrantCache: &resolveGrantCache,
resolveWalletCache: &resolveWalletCache,
apiAccessKeySignerCache: &apiAccessKeySignerCache,
oauthTokenCache: &oauthTokenCache,
requestValidator: requestValidator,
rewardAttester: rewardAttester,
transactionSender: transactionSender,
Expand Down Expand Up @@ -541,6 +550,12 @@ func NewApiServer(config config.Config) *ApiServer {
g.Post("/developer_apps/:address/access-keys", app.postV1UsersDeveloperAppAccessKey)
g.Post("/developer-apps/:address/access-keys", app.postV1UsersDeveloperAppAccessKey)

// OAuth2 PKCE
g.Post("/oauth/authorize", app.v1OAuthAuthorize)
g.Post("/oauth/token", app.v1OAuthToken)
g.Post("/oauth/revoke", app.v1OAuthRevoke)
g.Get("/oauth/me", app.requireAuthMiddleware, app.v1OAuthMe)

// Rewards
g.Post("/rewards/claim", app.v1ClaimRewards)
g.Post("/rewards/code", app.v1CreateRewardCode)
Expand Down Expand Up @@ -737,6 +752,7 @@ type ApiServer struct {
resolveGrantCache *otter.Cache[string, bool]
resolveWalletCache *otter.Cache[string, int]
apiAccessKeySignerCache *otter.Cache[string, apiAccessKeySignerEntry]
oauthTokenCache *otter.Cache[string, oauthTokenCacheEntry]
requestValidator *RequestValidator
rewardManagerClient *reward_manager.RewardManagerClient
claimableTokensClient *claimable_tokens.ClaimableTokensClient
Expand Down
Loading