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
29 changes: 21 additions & 8 deletions api/auth_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,15 +251,28 @@ 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 {
if myId == 0 || entry.UserID == myId {
wallet = strings.ToLower(entry.ClientID)
if myId == 0 {
myId = entry.UserID
c.Locals("myId", int(entry.UserID))
}
}
}
Expand Down
43 changes: 38 additions & 5 deletions api/request_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,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 @@ -132,7 +137,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 Down Expand Up @@ -165,3 +170,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