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
72 changes: 50 additions & 22 deletions api/auth_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,49 +246,77 @@ func (app *ApiServer) validateOAuthJWTTokenToWalletAndUserId(ctx context.Context
// - the user is not authorized to act on behalf of "myId"
// - the user is not authorized to act on behalf of "myWallet"
func (app *ApiServer) authMiddleware(c *fiber.Ctx) error {

// Try to populate the authorized wallet from the Authorization header or signature headers. The authMiddleware is designed to be flexible and support multiple auth methods, so it will attempt to resolve the authed wallet from multiple sources in the following order:
// 1a. Static dev app Bearer token or secret (e.g. api_access_key or AudiusApiSecret) - highest precedence since it's the most explicit
// 1b. OAuth 2.0 access token lookup - allows support for OAuth clients to do read/writes
// 2. OAuth JWT Bearer token - used by older clients with the implicit signed JWTs to do auth
// 3. OAuth 2.0 access token lookup - for cases where the dev app doesn't have their secret stored on API server - will only work for reads, not writes
// 4. Signature headers - legacy method used for reads
var wallet string

// 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)
} else {
wallet = app.recoverAuthorityFromSignatureHeaders(c)
// 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 "))
}

// OAuth JWT fallback: when Bearer token is not api_access_key, try as OAuth JWT (Plans app)
if wallet == "" && myId != 0 && bearerToken != "" {
if oauthWallet, jwtUserId, err := app.validateOAuthJWTTokenToWalletAndUserId(c.Context(), bearerToken); err == nil {
if int32(jwtUserId) == myId {
wallet = oauthWallet
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 {
if int32(jwtUserId) == myId {
app.logger.Debug("authMiddleware: resolved via OAuth JWT", zap.String("wallet", oauthWallet), zap.Int32("myId", myId))
wallet = oauthWallet
} else {
app.logger.Warn("authMiddleware: OAuth JWT userId does not match myId", zap.Int32("jwtUserId", int32(jwtUserId)), zap.Int32("myId", myId))
}
} else {
app.logger.Warn("authMiddleware: OAuth JWT userId does not match myId", zap.Int32("jwtUserId", int32(jwtUserId)), zap.Int32("myId", myId))
app.logger.Warn("authMiddleware: OAuth JWT validation failed", zap.Error(err))
}
} else {
app.logger.Warn("authMiddleware: OAuth JWT validation failed", zap.Error(err))
}
}
// 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)
c.Locals("oauthScope", entry.Scope)
if myId == 0 {
myId = entry.UserID
c.Locals("myId", int(entry.UserID))

// 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 {
if myId == 0 || entry.UserID == myId {
wallet = strings.ToLower(entry.ClientID)
c.Locals("oauthScope", entry.Scope)
if myId == 0 {
myId = entry.UserID
c.Locals("myId", int(entry.UserID))
}
app.logger.Debug("authMiddleware: resolved via PKCE token", zap.String("wallet", wallet), zap.Int32("userId", myId))
} else {
app.logger.Warn("authMiddleware: PKCE token userId does not match myId", zap.Int32("tokenUserId", entry.UserID), zap.Int32("myId", myId))
}
} else {
app.logger.Warn("authMiddleware: PKCE token userId does not match myId", zap.Int32("tokenUserId", entry.UserID), zap.Int32("myId", myId))
app.logger.Debug("authMiddleware: PKCE token lookup failed")
}
} else {
app.logger.Debug("authMiddleware: PKCE token lookup failed")
}
}

if wallet == "" {
// Try to get signer of headers for legacy signed requests
wallet = app.recoverAuthorityFromSignatureHeaders(c)
if wallet != "" {
app.logger.Debug("authMiddleware: resolved via signature headers", zap.String("wallet", wallet))
}
}

// If still no wallet, we couldn't resolve an authed wallet from the request
if wallet == "" {
app.logger.Debug("authMiddleware: no auth resolved")
}
}

c.Locals("authedWallet", wallet)
Expand Down
8 changes: 8 additions & 0 deletions api/request_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ func (app *ApiServer) getSignerFromOAuthToken(c *fiber.Ctx, token string) *Signe
if err != nil {
return nil
}

// Populate oauthScope and myId on the context so authMiddleware doesn't need
// a second PKCE token lookup when the signer was resolved via an OAuth token.
c.Locals("oauthScope", entry.Scope)
if myId, _ := c.Locals("myId").(int); myId == 0 {
c.Locals("myId", int(entry.UserID))
}

return &Signer{
Address: strings.ToLower(entry.ClientID),
PrivateKey: privateKey,
Expand Down
30 changes: 30 additions & 0 deletions api/v1_oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1246,3 +1246,33 @@ func TestAuthMiddleware_PKCEToken_UserIDMismatch(t *testing.T) {
// Should be rejected because the token's user_id (100) does not match the requested user_id (200)
assert.Equal(t, 403, res.StatusCode)
}

// TestOAuthMe_WithApiSecret tests the /me endpoint when the PKCE token belongs to a developer
// app that also has an api_secret registered in api_keys. In this case getApiSigner resolves the
// signer via getSignerFromOAuthToken, which must populate myId and oauthScope so that /me can
// resolve the authenticated user without a second PKCE token lookup.
func TestOAuthMe_WithApiSecret(t *testing.T) {
app := emptyTestApp(t)
clientID := seedOAuthTestData(t, app)

// Register a private key as the api_secret for this developer app.
// Using the well-known ganache key; the address must match clientID.
const apiSecret = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
ensureApiKeysTables(t, app, context.Background())
_, err := app.writePool.Exec(context.Background(), `
INSERT INTO api_keys (api_key, api_secret, rps, rpm)
VALUES ($1, $2, 10, 500000)
ON CONFLICT (api_key) DO UPDATE SET api_secret = EXCLUDED.api_secret
`, clientID, apiSecret)
require.NoError(t, err)

accessToken, _ := insertTestTokens(t, app, clientID, 100, "read", "family-apisecret", time.Hour, 30*24*time.Hour)

status, body := oauthGetWithBearer(t, app, "/v1/me", accessToken)

assert.Equal(t, 200, status)
jsonAssert(t, body, map[string]any{
"data.handle": "oauthuser",
"data.name": "OAuth User",
})
}
Loading