Skip to main content

Music API Security: OAuth Token Storage, Scopes, and Data Privacy for Developers

Published on June 4, 2026

Music API Security: OAuth Token Storage, Scopes, and Data Privacy for Developers

Why Music API Security Matters for Developers

Quick answer: Music APIs handle personal data (listening habits, playlists, emails) and OAuth tokens that grant ongoing access to user accounts. A leaked token or over-permissioned scope turns your app into a liability.

Music streaming integrations sit in a unique spot. Unlike a simple read-only API, music service connections often persist for months or years. Users connect their accounts once and expect your app to keep working. That means you are storing long-lived refresh tokens that can access personal data indefinitely.

The attack surface is real:

  • Stolen refresh tokens let attackers impersonate users, read private playlists, and modify libraries
  • Over-broad scopes expose data your app never needed, increasing your liability under GDPR and similar regulations
  • Insecure token storage (client-side, plaintext databases, logs) is the most common vulnerability in music app integrations

Security is not optional for music API work. It is a product requirement.

OAuth 2.0 Flows Across Music Services

Quick answer: Most music services use OAuth 2.0 Authorization Code flow, but the details (PKCE support, token lifetimes, refresh behavior) vary significantly between platforms.

Every major streaming platform implements OAuth 2.0, but each one makes different choices about flow details, token lifetimes, and refresh mechanics. Here is how they compare:

FeatureService AService BService CService D
OAuth FlowAuth CodeAuth CodeAuth CodeAuth Code
PKCE SupportYesYesOptionalYes
Access Token Lifetime1 hour1 hourVaries24 hours
Refresh Token LifetimeIndefinite (with use)IndefiniteIndefinite14 days
Token Rotation on RefreshNoNoNoYes
Scopes Required for Playlistsplaylist-read-privatemusic.library.readreadonlyr_usr
Re-auth Trigger6 months unusedPassword changeToken revocation14-day expiry

These differences create real engineering challenges. A token refresh strategy that works for one service fails silently on another because refresh tokens expire after 14 days instead of lasting indefinitely.

The critical takeaway: you need per-service token management logic, or you need an abstraction layer that handles it for you.

Token Storage Best Practices

Quick answer: Store tokens server-side, encrypted at rest, with access scoped to the services that need them. Never store tokens in client-side code, browser storage, or application logs.

Server-Side Storage Patterns

The safest place for OAuth tokens is a server-side database with encryption at rest. Here is a proven pattern:

// Token storage schema
const tokenSchema = {
  userId: 'string',           // Your internal user ID
  service: 'string',          // e.g., 'spotify', 'apple_music'
  accessToken: 'encrypted',   // AES-256-GCM encrypted
  refreshToken: 'encrypted',  // AES-256-GCM encrypted
  expiresAt: 'timestamp',     // When the access token expires
  scopes: 'string[]',         // Granted scopes
  createdAt: 'timestamp',
  updatedAt: 'timestamp'
};

Encryption matters. Even if your database is compromised, encrypted tokens buy you time. Use AES-256-GCM with a key managed through your cloud provider's KMS (AWS KMS, Google Cloud KMS, Azure Key Vault):

const crypto = require('crypto');

const ENCRYPTION_KEY = process.env.TOKEN_ENCRYPTION_KEY; // 32-byte key from KMS
const IV_LENGTH = 16;

function encryptToken(token) {
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
  let encrypted = cipher.update(token, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  const authTag = cipher.getAuthTag().toString('hex');
  return `${iv.toString('hex')}:${authTag}:${encrypted}`;
}

function decryptToken(encryptedData) {
  const [ivHex, authTagHex, encrypted] = encryptedData.split(':');
  const decipher = crypto.createDecipheriv(
    'aes-256-gcm',
    Buffer.from(ENCRYPTION_KEY, 'hex'),
    Buffer.from(ivHex, 'hex')
  );
  decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
}

What to avoid:

  • LocalStorage or SessionStorage: Accessible to any JavaScript running on your domain (XSS = full token theft)
  • Cookies without flags: Missing HttpOnly, Secure, and SameSite flags exposes tokens to script access and CSRF
  • Plaintext database columns: A database breach hands over every token in your system
  • Application logs: Tokens in log output end up in log aggregators, third-party monitoring tools, and crash reports

Token Refresh Strategies

Access tokens expire. Your refresh strategy determines whether users see seamless access or broken connections.

async function getValidToken(userId, service) {
  const stored = await db.tokens.findOne({ userId, service });
  if (!stored) throw new Error('No token found. User must re-authenticate.');

  const accessToken = decryptToken(stored.accessToken);
  const refreshToken = decryptToken(stored.refreshToken);

  // Refresh proactively: 5 minutes before expiry
  const bufferMs = 5 * 60 * 1000;
  if (stored.expiresAt.getTime() - Date.now() > bufferMs) {
    return accessToken;
  }

  // Token expired or expiring soon: refresh it
  const newTokens = await refreshAccessToken(service, refreshToken);

  await db.tokens.updateOne(
    { userId, service },
    {
      accessToken: encryptToken(newTokens.access_token),
      refreshToken: encryptToken(newTokens.refresh_token || refreshToken),
      expiresAt: new Date(Date.now() + newTokens.expires_in * 1000),
      updatedAt: new Date()
    }
  );

  return newTokens.access_token;
}

Key patterns:

  • Proactive refresh: Refresh tokens 5 minutes before expiry, not after they fail. This prevents request failures.
  • Handle rotation: Some services issue a new refresh token on every refresh. Always store the latest one.
  • Retry with backoff: If a refresh fails due to rate limiting, retry with exponential backoff before forcing re-authentication.
  • Detect revocation: If a refresh returns a 401 or invalid_grant error, the user revoked access. Clear stored tokens and prompt re-authentication.

Managing refresh logic across multiple streaming services is tedious and error-prone. Each service has different expiry windows, rotation policies, and error responses. MusicAPI handles token refresh automatically across all supported services, so you store one connection reference instead of managing per-service refresh logic yourself.

Scopes and Permissions: What to Request and Why

Quick answer: Request the minimum scopes your app actually needs. Every extra scope increases your data liability and reduces user trust.

OAuth scopes control what your app can access. The principle of least privilege applies directly: request only what you need, and document why.

Common scope categories across music services:

Scope CategoryWhat It GrantsWhen to Request
Read playlistsView user's playlists and tracksPlaylist display, migration, analysis
Write playlistsCreate and modify playlistsPlaylist creation, track adding
Read libraryView saved/favorited tracksMusic taste analysis, recommendations
Write libraryAdd/remove favoritesSyncing favorites across services
Read profileView user's display name, emailAccount linking, personalization
StreamingControl playbackPlayer apps only
Read listening historyView recently played tracksAnalytics, recommendations

Scope strategy rules:

  1. Start minimal. Launch with read-only scopes. Add write scopes only when users take an action that requires them.
  2. Request at the moment of need. Do not ask for playlist-write permission during onboarding if the user has not tried to create a playlist yet. Use incremental authorization.
  3. Document your scope usage. Maintain an internal document mapping each scope to the feature that requires it. When a feature is removed, remove the scope.
  4. Audit quarterly. Review which scopes you request vs. which endpoints you actually call. Remove any scope that is not actively used.

This is easier when your API layer normalizes scope naming. MusicAPI maps service-specific scope names to consistent permission levels, so you define what access your app needs once instead of translating scopes per platform.

Data Privacy Compliance (GDPR, User Data Handling)

Quick answer: If your app serves EU users (and it probably does), you must handle music data under GDPR. That means consent, data minimization, access rights, and deletion capabilities.

Music streaming data is personal data under GDPR. Playlists reveal religious beliefs (gospel playlists), political opinions (protest music), health conditions (meditation/therapy playlists), and sexual orientation. Courts have confirmed that listening data qualifies as sensitive personal information.

GDPR requirements for music API integrations:

  1. Lawful basis for processing. You need explicit user consent to access their music data. The OAuth authorization screen counts as consent for the scopes displayed, but you must also explain in your privacy policy what you do with the data.

  2. Data minimization. Only collect and store the data you need. If your app displays playlists but does not analyze listening history, do not store track-play timestamps.

  3. Right to access (Article 15). Users can request a copy of all music data you hold about them. Build an export endpoint:

app.get('/api/user/:userId/data-export', async (req, res) => {
  const userData = await db.users.findOne({ id: req.params.userId });
  const connections = await db.tokens.find(
    { userId: req.params.userId },
    { projection: { accessToken: 0, refreshToken: 0 } } // Never export tokens
  );
  const playlists = await db.playlists.find({ userId: req.params.userId });

  res.json({
    profile: userData,
    connectedServices: connections,
    playlists: playlists,
    exportedAt: new Date().toISOString()
  });
});
  1. Right to erasure (Article 17). Users can request deletion of all their data. This must include revoking OAuth tokens with the streaming services:
app.delete('/api/user/:userId/data', async (req, res) => {
  const tokens = await db.tokens.find({ userId: req.params.userId });

  // Revoke tokens with each connected service
  for (const token of tokens) {
    await revokeServiceToken(token.service, decryptToken(token.refreshToken));
  }

  // Delete all user data
  await db.tokens.deleteMany({ userId: req.params.userId });
  await db.playlists.deleteMany({ userId: req.params.userId });
  await db.users.deleteOne({ id: req.params.userId });

  res.json({ deleted: true, deletedAt: new Date().toISOString() });
});
  1. Data retention limits. Set automatic expiry on stored music data. If a user has not opened your app in 12 months, delete their tokens and cached data.

  2. Third-party disclosure. If you send music data to analytics providers, recommendation engines, or any third party, disclose this in your privacy policy and ensure those providers are GDPR-compliant.

How MusicAPI Simplifies Auth Security

Quick answer: MusicAPI handles OAuth flows, token storage, automatic refresh, and scope normalization across all supported streaming services through one unified authentication system.

Building secure OAuth integrations with multiple music services means solving the same problems repeatedly: different token lifetimes, different refresh behaviors, different scope naming, different error responses. MusicAPI eliminates that duplication.

What MusicAPI handles for you:

  • One OAuth flow for all services. Initialize authentication once. MusicAPI manages the service-specific OAuth details, including PKCE where required.
  • Automatic token refresh. No per-service refresh logic. MusicAPI detects expiring tokens and refreshes them before your API calls fail.
  • Secure token management. Tokens are stored and encrypted on MusicAPI's infrastructure. Your app receives a connection identifier, not raw OAuth tokens. If you need original tokens for direct API calls, you can request them securely.
  • Normalized scopes. Instead of mapping playlist-read-private vs. music.library.read vs. r_usr across services, you work with consistent permission models.
  • One callback endpoint. Replace per-service callback handlers with a single, standardized callback.
  • Built-in rate limiting. MusicAPI respects per-service rate limits so your app does not get throttled or banned.

The result: your security surface area shrinks from N services to one integration point. Less code means fewer places for security bugs to hide.

FAQ

Should I store OAuth tokens on the client or the server?

Always store tokens on the server. Client-side storage (LocalStorage, SessionStorage, cookies without HttpOnly) is vulnerable to XSS attacks. A single cross-site scripting vulnerability would expose every user's music service tokens. Keep tokens server-side, encrypted at rest, and issue your own session tokens to the client.

How often should I refresh access tokens?

Refresh proactively, about 5 minutes before expiry. Do not wait for a 401 response; that creates a failed request the user might notice. Most music services issue access tokens that last 1 hour, so schedule your refresh at the 55-minute mark. If you use MusicAPI, token refresh is automatic.

What happens when a user revokes access from the streaming service side?

Your refresh token becomes invalid. The next refresh attempt returns an invalid_grant or 401 error. When this happens, clear the stored tokens for that user and service, then prompt the user to re-authenticate. Do not retry the refresh; the token is permanently invalid.

Do I need GDPR compliance if my app only reads playlists?

Yes. Playlist data is personal data under GDPR. Playlist names, track lists, and creation dates reveal personal preferences and habits. If you serve EU users (which is likely for any public app), you need lawful basis for processing, data minimization, and the ability to export and delete user data on request.

How do I handle token storage for multiple music services per user?

Store tokens per user-service pair. Each row in your tokens table should have a userId, service identifier, encrypted access token, encrypted refresh token, and expiry timestamp. Index on (userId, service) for fast lookups. With MusicAPI, you store a single connection ID per service instead of managing raw tokens yourself.

What scopes should I request for a playlist migration app?

Request read-playlist and read-library scopes on the source service, and write-playlist scopes on the destination service. Do not request streaming, playback, or profile-write scopes unless your app specifically needs them. Check the supported features page to see which operations are available per service.

How do I audit which scopes my app actually uses?

Log every API endpoint your app calls per service (without logging tokens or user data). After 30 days, compare the endpoints used against the scopes requested. Any scope not tied to an active endpoint should be removed. This reduces your data liability and improves user trust during the OAuth consent screen.


Ready to skip months of OAuth and SDK work? Start your free MusicAPI trial and connect 10+ streaming services with one unified API.