NVLP Logo Authentication Guide

Complete guide to authenticating with the NVLP API using magic links

🔐 Authentication Overview

NVLP uses magic link authentication powered by Supabase Auth. This provides a secure, passwordless authentication method that's both user-friendly and developer-friendly.

🎯 Key Benefits

  • Passwordless - No passwords to remember or manage
  • Secure - JWT tokens with automatic expiration
  • Mobile-friendly - Works with deep links for mobile apps
  • User-friendly - Simple email-based authentication

Authentication Endpoints

Endpoint Method Purpose
/functions/v1/auth-magic-link POST Send magic link email
/functions/v1/auth-user GET Get current user profile
/functions/v1/auth-user-update PATCH Update user profile
/functions/v1/auth-logout POST Sign out user

⚙️ Implementation Examples

JavaScript/TypeScript

class NVLPAuth {
  constructor(supabaseUrl, anonKey) {
    this.supabaseUrl = supabaseUrl;
    this.anonKey = anonKey;
    this.accessToken = this.getStoredToken();
  }
  
  // Send magic link
  async sendMagicLink(email, redirectTo = window.location.origin + '/auth/callback') {
    const response = await fetch(`${this.supabaseUrl}/functions/v1/auth-magic-link`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.anonKey}`
      },
      body: JSON.stringify({ email, redirectTo })
    });
    
    if (!response.ok) {
      throw new Error(`Auth error: ${response.status}`);
    }
    
    return response.json();
  }
  
  // Get current user
  async getCurrentUser() {
    if (!this.accessToken) {
      throw new Error('No access token available');
    }
    
    const response = await fetch(`${this.supabaseUrl}/functions/v1/auth-user`, {
      headers: {
        'Authorization': `Bearer ${this.accessToken}`
      }
    });
    
    if (!response.ok) {
      if (response.status === 401) {
        await this.refreshToken();
        return this.getCurrentUser(); // Retry with new token
      }
      throw new Error(`Failed to get user: ${response.status}`);
    }
    
    return response.json();
  }
  
  // Make authenticated API request
  async apiRequest(endpoint, options = {}) {
    const headers = {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${this.accessToken}`,
      'apikey': this.anonKey,
      ...options.headers
    };
    
    const response = await fetch(`${this.supabaseUrl}${endpoint}`, {
      ...options,
      headers
    });
    
    if (response.status === 401) {
      await this.refreshToken();
      // Retry request with new token
      headers['Authorization'] = `Bearer ${this.accessToken}`;
      return fetch(`${this.supabaseUrl}${endpoint}`, { ...options, headers });
    }
    
    return response;
  }
  
  // Token management
  getStoredToken() {
    return localStorage.getItem('nvlp_access_token');
  }
  
  setToken(accessToken, refreshToken) {
    this.accessToken = accessToken;
    localStorage.setItem('nvlp_access_token', accessToken);
    if (refreshToken) {
      localStorage.setItem('nvlp_refresh_token', refreshToken);
    }
  }
  
  async refreshToken() {
    // Implement token refresh logic here
    // This would typically involve calling Supabase's refresh endpoint
    console.warn('Token refresh not implemented - user may need to re-authenticate');
  }
  
  logout() {
    this.accessToken = null;
    localStorage.removeItem('nvlp_access_token');
    localStorage.removeItem('nvlp_refresh_token');
  }
}

// Usage example
const auth = new NVLPAuth('https://your-project.supabase.co', 'your-anon-key');

// Send magic link
await auth.sendMagicLink('user@example.com');

// After user clicks link and tokens are extracted
auth.setToken(extractedAccessToken, extractedRefreshToken);

// Make authenticated requests
const user = await auth.getCurrentUser();
const budgets = await auth.apiRequest('/rest/v1/budgets');

Python

import requests
import json
from typing import Optional, Dict, Any

class NVLPAuth:
    def __init__(self, supabase_url: str, anon_key: str):
        self.supabase_url = supabase_url
        self.anon_key = anon_key
        self.access_token: Optional[str] = None
    
    def send_magic_link(self, email: str, redirect_to: str) -> Dict[str, Any]:
        """Send magic link to user's email"""
        response = requests.post(
            f"{self.supabase_url}/functions/v1/auth-magic-link",
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {self.anon_key}"
            },
            json={"email": email, "redirectTo": redirect_to}
        )
        response.raise_for_status()
        return response.json()
    
    def set_token(self, access_token: str):
        """Set the access token for authenticated requests"""
        self.access_token = access_token
    
    def get_current_user(self) -> Dict[str, Any]:
        """Get current user profile"""
        if not self.access_token:
            raise ValueError("No access token set")
        
        response = requests.get(
            f"{self.supabase_url}/functions/v1/auth-user",
            headers={"Authorization": f"Bearer {self.access_token}"}
        )
        response.raise_for_status()
        return response.json()
    
    def api_request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> requests.Response:
        """Make authenticated API request"""
        if not self.access_token:
            raise ValueError("No access token set")
        
        headers = {
            "Authorization": f"Bearer {self.access_token}",
            "apikey": self.anon_key,
            "Content-Type": "application/json"
        }
        
        response = requests.request(
            method=method,
            url=f"{self.supabase_url}{endpoint}",
            headers=headers,
            json=data
        )
        
        return response

# Usage example
auth = NVLPAuth("https://your-project.supabase.co", "your-anon-key")

# Send magic link
auth.send_magic_link("user@example.com", "https://myapp.com/callback")

# After token extraction from callback
auth.set_token("extracted_access_token")

# Make authenticated requests
user = auth.get_current_user()
budgets_response = auth.api_request("/rest/v1/budgets")

cURL Examples

# Send magic link
curl -X POST "https://your-project.supabase.co/functions/v1/auth-magic-link" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_ANON_KEY" \
  -d '{
    "email": "user@example.com",
    "redirectTo": "https://myapp.com/auth/callback"
  }'

# Get current user (after obtaining access token)
curl -X GET "https://your-project.supabase.co/functions/v1/auth-user" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# Make authenticated API request
curl -X GET "https://your-project.supabase.co/rest/v1/budgets" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "apikey: YOUR_ANON_KEY"

🔄 Token Management

Proper token management is crucial for a good user experience and security:

Token Storage

⚠️ Security Considerations

  • Web Apps: Store tokens in localStorage or sessionStorage (not cookies for XSS protection)
  • Mobile Apps: Use secure storage (iOS Keychain, Android Keystore)
  • Server-side: Store in secure, encrypted databases
  • Never: Store tokens in plain text files or unencrypted storage

Token Expiration & Refresh

NVLP uses JWT tokens that expire after a certain time. Here's how to handle token expiration:

// Check if token is expired
function isTokenExpired(token) {
  try {
    const payload = JSON.parse(atob(token.split('.')[1]));
    const currentTime = Math.floor(Date.now() / 1000);
    return payload.exp < currentTime;
  } catch (error) {
    return true; // Assume expired if can't parse
  }
}

// Automatic token refresh wrapper
async function makeAuthenticatedRequest(url, options = {}) {
  let token = getStoredToken();
  
  // Check if token is expired
  if (!token || isTokenExpired(token)) {
    // Redirect to login or refresh token
    window.location.href = '/login';
    return;
  }
  
  const response = await fetch(url, {
    ...options,
    headers: {
      'Authorization': `Bearer ${token}`,
      ...options.headers
    }
  });
  
  // Handle 401 (unauthorized) responses
  if (response.status === 401) {
    // Token might have expired, redirect to login
    window.location.href = '/login';
    return;
  }
  
  return response;
}

Logout Implementation

async function logout() {
  const token = getStoredToken();
  
  if (token) {
    try {
      // Call logout endpoint to invalidate token on server
      await fetch('https://your-project.supabase.co/functions/v1/auth-logout', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`
        }
      });
    } catch (error) {
      console.warn('Logout request failed:', error);
      // Continue with local cleanup even if server request fails
    }
  }
  
  // Clear local storage
  localStorage.removeItem('nvlp_access_token');
  localStorage.removeItem('nvlp_refresh_token');
  
  // Redirect to login page
  window.location.href = '/login';
}

🛡️ Security Best Practices

Token Security

  • HTTPS Only: Always use HTTPS in production
  • Secure Storage: Use appropriate secure storage for your platform
  • Token Rotation: Implement token refresh mechanisms
  • Logout Cleanup: Clear tokens on logout

Magic Link Security

  • One-time Use: Magic links should only work once
  • Time Limits: Links should expire (typically 1 hour)
  • Secure Redirect: Validate redirect URLs to prevent phishing
  • Rate Limiting: Limit magic link requests per email

✅ NVLP Built-in Security

NVLP implements these security measures automatically:

  • Magic links expire after 1 hour
  • Links are single-use only
  • Rate limiting on magic link requests
  • Secure JWT token generation
  • Row-level security on all data

Environment Variables

🔐 Never Expose Secret Keys

Only use the anon key in client-side code. Never expose the service role key in client applications.

# Safe for client-side use
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# NEVER use in client-side code
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # Server-only!

🔧 Troubleshooting

Common Issues

Magic Link Not Received

  • Check spam/junk folder
  • Verify email address is correct
  • Check email provider isn't blocking emails
  • Verify Supabase email settings are configured

401 Unauthorized Errors

  • Check if access token is included in request headers
  • Verify token hasn't expired
  • Ensure correct Authorization: Bearer TOKEN format
  • Check if user has permission to access the resource

CORS Issues

  • Verify your domain is in Supabase allowed origins
  • Check if you're using the correct Supabase URL
  • Ensure HTTPS is used in production

Testing Authentication

Use our test script to verify your authentication setup:

# Download and run auth test script
curl -o test-auth.sh https://nvlp.app/scripts/test-auth.sh
chmod +x test-auth.sh

# Set your environment variables
export SUPABASE_URL="https://your-project.supabase.co"
export SUPABASE_ANON_KEY="your-anon-key"
export TEST_EMAIL="your-email@example.com"

# Run the test
./test-auth.sh

Debug Mode

Enable debug logging to troubleshoot authentication issues:

// Enable debug mode
const auth = new NVLPAuth(supabaseUrl, anonKey);
auth.debug = true; // This will log all requests and responses

// Or add manual logging
console.log('Sending magic link request...');
const result = await auth.sendMagicLink(email);
console.log('Magic link response:', result);

Getting Help

📞 Support Resources

📚 Next Steps

Now that you understand authentication, explore these resources: