Skip to content

OAuth Integration

This document details how Quizzical Beats integrates with OAuth providers, including Spotify and Dropbox.

Overview

Quizzical Beats uses OAuth 2.0 to authenticate with third-party services. The current OAuth implementation provides:

  • API access to third-party services (Spotify API, Dropbox files)
  • Token storage and refresh mechanisms
  • Fallback strategies when tokens expire

OAuth Provider Configuration

Spotify OAuth

Spotify OAuth is used for API access:

SPOTIFY_CLIENT_ID = os.environ.get('SPOTIFY_CLIENT_ID')
SPOTIFY_CLIENT_SECRET = os.environ.get('SPOTIFY_CLIENT_SECRET')
SPOTIFY_REDIRECT_URI = os.environ.get('SPOTIFY_REDIRECT_URI', 'http://localhost:5000/auth/spotify/callback')

Dropbox OAuth

Dropbox OAuth enables file export functionality:

DROPBOX_APP_KEY = os.environ.get('DROPBOX_APP_KEY')
DROPBOX_APP_SECRET = os.environ.get('DROPBOX_APP_SECRET')
DROPBOX_REDIRECT_URI = os.environ.get('DROPBOX_REDIRECT_URI', 'http://localhost:5000/users/dropbox/callback')

Dropbox Integration Implementation

The Dropbox OAuth integration is implemented directly in the User model:

class User(db.Model):
    # Other user fields...

    # Dropbox OAuth fields
    dropbox_id = db.Column(db.String(100), nullable=True)
    dropbox_token = db.Column(db.Text(), nullable=True)
    dropbox_refresh_token = db.Column(db.Text(), nullable=True)
    dropbox_token_expiry = db.Column(db.DateTime(), nullable=True)
    dropbox_export_path = db.Column(db.String(255), nullable=True)

Dropbox Authentication Flow

  1. User initiates Dropbox connection from their profile page
  2. Application redirects to Dropbox's authorization page
  3. User grants permission to the application
  4. Dropbox redirects back to our callback URL with an authorization code
  5. Application exchanges the code for access and refresh tokens
  6. Tokens and basic user info are stored in the user's record

Example of the callback handler:

@users_bp.route('/dropbox/callback')
@login_required
def dropbox_callback():
    # Handle errors
    if 'error' in request.args:
        flash(f'Dropbox authorization failed: {error}', 'error')
        return redirect(url_for('users.profile'))

    # Exchange authorization code for tokens
    code = request.args.get('code')
    token_info = exchange_code_for_token(code)

    # Store tokens in the user model
    current_user.dropbox_token = token_info.get('access_token')
    current_user.dropbox_refresh_token = token_info.get('refresh_token')

    # Store expiration time
    expires_in = token_info.get('expires_in', 14400)  # Default to 4 hours
    current_user.dropbox_token_expiry = datetime.now() + timedelta(seconds=expires_in)

    # Get and store account info
    account_info = get_dropbox_account_info(current_user.dropbox_token)
    if account_info:
        current_user.dropbox_id = account_info.get('account_id')

    db.session.commit()

    return redirect(url_for('users.profile'))

Token Management

Token Refresh

Tokens are refreshed when they expire. The Dropbox implementation uses:

def get_current_user_dropbox_token():
    """Get a valid Dropbox access token for the current user, refreshing if needed"""
    if not current_user or not current_user.is_authenticated:
        return None

    # Check if token exists and is valid
    if (current_user.dropbox_token and 
        current_user.dropbox_token_expiry and 
        current_user.dropbox_token_expiry > datetime.now() + timedelta(minutes=5)):
        return current_user.dropbox_token

    # Token is missing or about to expire - try to refresh
    if current_user.dropbox_refresh_token:
        # Refresh the token
        token_info = refresh_dropbox_token(current_user.dropbox_refresh_token)

        if token_info and 'access_token' in token_info:
            # Update token in database
            current_user.dropbox_token = token_info['access_token']
            expires_in = token_info.get('expires_in', 14400)
            current_user.dropbox_token_expiry = datetime.now() + timedelta(seconds=expires_in)

            db.session.commit()

            return current_user.dropbox_token

    return None

Token Revocation

Users can disconnect their Dropbox accounts:

@users_bp.route('/dropbox/disconnect', methods=['POST'])
@login_required
def dropbox_disconnect():
    """Disconnect user's Dropbox account"""
    # Revoke token if present
    if current_user.dropbox_token:
        try:
            revoke_token(current_user.dropbox_token)
        except Exception as e:
            # Log the error but continue
            pass

    # Clear Dropbox credentials
    current_user.dropbox_token = None
    current_user.dropbox_refresh_token = None
    current_user.dropbox_token_expiry = None
    current_user.dropbox_id = None

    db.session.commit()

    return redirect(url_for('users.profile'))

Security Considerations

When working with OAuth:

  • Always use HTTPS in production
  • Store tokens securely
  • Implement proper token refresh
  • Handle token revocation when users disconnect accounts
  • Request minimal scope access
  • Validate all OAuth-related inputs
  • Use the official provider documentation for the most up-to-date OAuth implementation details