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
128 changes: 128 additions & 0 deletions api/auth_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ package api

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"
"time"

comms "api.audius.co/api/comms"
"api.audius.co/trashid"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/gofiber/fiber/v2"
Expand Down Expand Up @@ -98,6 +101,131 @@ func (app *ApiServer) getAuthedWallet(c *fiber.Ctx) string {
return c.Locals("authedWallet").(string)
}

// validateOAuthJWTTokenToUserId validates the OAuth JWT and returns the userId from the payload.
func (app *ApiServer) validateOAuthJWTTokenToUserId(ctx context.Context, token string) (trashid.HashId, error) {
tokenParts := strings.Split(token, ".")
if len(tokenParts) != 3 {
return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid JWT token format")
}

base64Header := tokenParts[0]
base64Payload := tokenParts[1]
base64Signature := tokenParts[2]

paddedSignature := base64Signature
if len(paddedSignature)%4 != 0 {
paddedSignature += strings.Repeat("=", 4-len(paddedSignature)%4)
}
signatureDecoded, err := base64.URLEncoding.DecodeString(paddedSignature)
if err != nil {
return 0, fiber.NewError(fiber.StatusBadRequest, "The JWT signature could not be decoded")
}
signatureHex := string(signatureDecoded)
signatureBytes := common.FromHex(signatureHex)

message := fmt.Sprintf("%s.%s", base64Header, base64Payload)
encodedToRecover := []byte(message)
prefixedMessage := []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(encodedToRecover), encodedToRecover))
finalHash := crypto.Keccak256Hash(prefixedMessage)

if len(signatureBytes) != 65 {
return 0, fiber.NewError(fiber.StatusBadRequest, "The JWT signature was incorrectly signed")
}
if signatureBytes[64] >= 27 {
signatureBytes[64] -= 27
}
publicKey, err := crypto.SigToPub(finalHash.Bytes(), signatureBytes)
if err != nil {
return 0, fiber.NewError(fiber.StatusUnauthorized, "The JWT signature is invalid")
}
recoveredAddr := crypto.PubkeyToAddress(*publicKey)
walletLower := strings.ToLower(recoveredAddr.Hex())

paddedPayload := base64Payload
if len(paddedPayload)%4 != 0 {
paddedPayload += strings.Repeat("=", 4-len(paddedPayload)%4)
}
stringifiedPayload, err := base64.URLEncoding.DecodeString(paddedPayload)
if err != nil {
return 0, fiber.NewError(fiber.StatusBadRequest, "JWT payload could not be decoded")
}
var payload map[string]interface{}
if err := json.Unmarshal(stringifiedPayload, &payload); err != nil {
return 0, fiber.NewError(fiber.StatusBadRequest, "JWT payload could not be unmarshalled")
}

userIdInterface, exists := payload["userId"]
if !exists {
return 0, fiber.NewError(fiber.StatusBadRequest, "JWT payload missing userId field")
}
userIdStr, ok := userIdInterface.(string)
if !ok {
return 0, fiber.NewError(fiber.StatusBadRequest, "JWT payload userId must be a string")
}
jwtUserId, err := trashid.DecodeHashId(userIdStr)
if err != nil {
return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid JWT payload userId")
}

walletUserId, err := app.queries.GetUserForWallet(ctx, walletLower)
if err != nil {
if err == pgx.ErrNoRows {
return 0, fiber.NewError(fiber.StatusUnauthorized, "The JWT signature is invalid - invalid wallet")
}
return 0, err
}

if int32(walletUserId) != int32(jwtUserId) {
isManager, err := app.isActiveManager(ctx, int32(jwtUserId), int32(walletUserId))
if err != nil {
return 0, err
}
if !isManager {
return 0, fiber.NewError(fiber.StatusForbidden, "The JWT signature is invalid - the wallet does not match the user")
}
}

return trashid.HashId(jwtUserId), nil
}

// validateOAuthJWTTokenToWalletAndUserId validates the OAuth JWT and returns (wallet, userId).
// Used by auth middleware when Bearer token is not an api_access_key.
func (app *ApiServer) validateOAuthJWTTokenToWalletAndUserId(ctx context.Context, token string) (wallet string, userId int32, err error) {
id, err := app.validateOAuthJWTTokenToUserId(ctx, token)
if err != nil {
return "", 0, err
}
tokenParts := strings.Split(token, ".")
if len(tokenParts) != 3 {
return "", 0, fiber.NewError(fiber.StatusBadRequest, "Invalid JWT token format")
}
base64Payload, base64Signature := tokenParts[1], tokenParts[2]
paddedSignature := base64Signature
if len(paddedSignature)%4 != 0 {
paddedSignature += strings.Repeat("=", 4-len(paddedSignature)%4)
}
signatureDecoded, err := base64.URLEncoding.DecodeString(paddedSignature)
if err != nil {
return "", 0, fiber.NewError(fiber.StatusBadRequest, "The JWT signature could not be decoded")
}
signatureBytes := common.FromHex(string(signatureDecoded))
if len(signatureBytes) != 65 {
return "", 0, fiber.NewError(fiber.StatusBadRequest, "The JWT signature was incorrectly signed")
}
if signatureBytes[64] >= 27 {
signatureBytes[64] -= 27
}
message := fmt.Sprintf("%s.%s", tokenParts[0], base64Payload)
prefixedMessage := []byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(message), message))
finalHash := crypto.Keccak256Hash(prefixedMessage)
publicKey, err := crypto.SigToPub(finalHash.Bytes(), signatureBytes)
if err != nil {
return "", 0, fiber.NewError(fiber.StatusUnauthorized, "The JWT signature is invalid")
}
recoveredAddr := crypto.PubkeyToAddress(*publicKey)
return strings.ToLower(recoveredAddr.Hex()), int32(id), nil
}

// Middleware to set authedUserId and authedWallet in context
// Returns a 403 if either
// - the user is not authorized to act on behalf of "myId"
Expand Down
4 changes: 3 additions & 1 deletion api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -526,11 +526,13 @@ func NewApiServer(config config.Config) *ApiServer {
g.Get("/search/full", app.v1SearchFull)
g.Get("/search/tags", app.v1SearchFull)

// Developer Apps (Plans API - user_id query; indexer validates grants)
// Developer Apps
g.Get("/developer_apps/:address", app.v1DeveloperApps)
g.Get("/developer-apps/:address", app.v1DeveloperApps)
g.Post("/developer_apps", app.postV1UsersDeveloperApp)
g.Post("/developer-apps", app.postV1UsersDeveloperApp)
g.Put("/developer_apps/:address", app.putV1UsersDeveloperApp)
g.Put("/developer-apps/:address", app.putV1UsersDeveloperApp)
g.Delete("/developer_apps/:address", app.deleteV1UsersDeveloperApp)
g.Delete("/developer-apps/:address", app.deleteV1UsersDeveloperApp)
g.Post("/developer_apps/:address/access-keys/deactivate", app.postV1UsersDeveloperAppAccessKeyDeactivate)
Expand Down
67 changes: 55 additions & 12 deletions api/swagger/swagger-v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ paths:
post:
tags:
- developer_apps
description: Create a new developer app (Plans API). Indexer validates grants.
description: Create a new developer app. Indexer validates grants.
operationId: Create Developer App
parameters:
- name: user_id
Expand Down Expand Up @@ -622,7 +622,7 @@ paths:
delete:
tags:
- developer_apps
description: Deletes a developer app (Plans API). Indexer validates grants.
description: Deletes a developer app. Indexer validates grants.
operationId: Delete Developer App
parameters:
- name: user_id
Expand Down Expand Up @@ -653,11 +653,55 @@ paths:
"500":
description: Server error
content: {}
put:
tags:
- developer_apps
description: Updates a developer app. Indexer validates grants.
operationId: Update Developer App
parameters:
- name: user_id
in: query
description: The user ID of the user who owns the developer app
required: true
schema:
type: string
- name: address
in: path
description: Developer app address (API Key)
required: true
schema:
type: string
requestBody:
x-codegen-request-body-name: metadata
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/update_developer_app_request_body"
responses:
"200":
description: Developer app updated successfully
content:
application/json:
schema:
$ref: "#/components/schemas/write_response"
"400":
description: Bad request
content: {}
"401":
description: Unauthorized
content: {}
"404":
description: Developer app not found
content: {}
"500":
description: Server error
content: {}
/developer-apps/{address}/access-keys/deactivate:
post:
tags:
- developer_apps
description: Deactivate a bearer token (API access key) for a developer app (Plans API). Indexer validates grants.
description: Deactivate a bearer token (API access key) for a developer app. Indexer validates grants.
operationId: Deactivate Developer App Access Key
parameters:
- name: user_id
Expand Down Expand Up @@ -702,7 +746,7 @@ paths:
post:
tags:
- developer_apps
description: Create a new bearer token (API access key) for a developer app (Plans API). Indexer validates grants.
description: Create a new bearer token (API access key) for a developer app. Indexer validates grants.
operationId: Create Developer App Access Key
parameters:
- name: user_id
Expand Down Expand Up @@ -10328,7 +10372,7 @@ components:
type: string
description: Solana USDC payout wallet address
playlist_library:
$ref: '#/components/schemas/user_playlist_library'
$ref: "#/components/schemas/user_playlist_library"
events:
type: object
description: User events for tracking referrals and mobile users
Expand Down Expand Up @@ -10404,7 +10448,7 @@ components:
type: string
description: Coin flair mint address
playlist_library:
$ref: '#/components/schemas/user_playlist_library'
$ref: "#/components/schemas/user_playlist_library"
events:
type: object
description: User events for tracking referrals and mobile users
Expand Down Expand Up @@ -10584,7 +10628,7 @@ components:
properties:
name:
type: string
description: Developer app name (Plans API create)
description: Developer app name
example: "My API Key"
deactivate_access_key_request_body:
type: object
Expand Down Expand Up @@ -10653,9 +10697,9 @@ components:
description: Array of folders and playlist identifiers
items:
oneOf:
- $ref: '#/components/schemas/playlist_library_folder'
- $ref: '#/components/schemas/playlist_library_playlist_identifier'
- $ref: '#/components/schemas/playlist_library_explore_playlist_identifier'
- $ref: "#/components/schemas/playlist_library_folder"
- $ref: "#/components/schemas/playlist_library_playlist_identifier"
- $ref: "#/components/schemas/playlist_library_explore_playlist_identifier"
playlist_library_folder:
type: object
description: Folder containing nested playlists and folders
Expand Down Expand Up @@ -10854,6 +10898,5 @@ components:
scheme: bearer
bearerFormat: JWT
description: |
OAuth JWT Bearer token for Plans API. Used for user developer app create/delete.
Obtain via OAuth flow with write scope. The plans app must have a grant from the user.
The API bearer token or OAuth JWT token for the user.
x-original-swagger-version: "2.0"
Loading