Skip to content

Content Script

Access user authentication in content scripts.

Overview

Content scripts run in web pages and have limited access to Chrome APIs. This example shows how to:

  • Check authentication status from content scripts
  • Access user data in page context
  • Inject UI based on auth state
  • Communicate with background service worker

Architecture

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Web Page      │────▶│ Content Script  │────▶│ Background      │
│                 │     │ (Injected)      │     │ Service Worker  │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                                │                       │
                                │  chrome.runtime       │
                                │  .sendMessage()       │
                                └───────────────────────┘

Project Structure

content-auth/
├── manifest.json
├── background.js
├── content/
│   ├── content.js
│   └── content.css
├── popup/
│   └── popup.html
└── package.json

Manifest Configuration

json
{
  "manifest_version": 3,
  "name": "Content Auth Extension",
  "version": "1.0.0",

  "background": {
    "service_worker": "background.js",
    "type": "module"
  },

  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content/content.js"],
      "css": ["content/content.css"],
      "run_at": "document_end"
    }
  ],

  "action": {
    "default_popup": "popup/popup.html"
  },

  "permissions": [
    "storage"
  ],

  "host_permissions": [
    "https://api.extensionlogin.com/*"
  ]
}

Background Service Worker

javascript
// background.js
import ExtensionLogin from 'extensionlogin';

ExtensionLogin.init({
  apiKey: 'el_live_your_api_key_here',
  autoIdentify: true
});

// Handle messages from content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  handleMessage(message, sender).then(sendResponse);
  return true;
});

async function handleMessage(message, sender) {
  switch (message.type) {
    case 'GET_USER':
      return {
        success: true,
        user: ExtensionLogin.getUser()
      };

    case 'IS_AUTHENTICATED':
      return {
        authenticated: ExtensionLogin.isAuthenticated()
      };

    case 'IDENTIFY':
      return await ExtensionLogin.identify(message.data);

    case 'LOGOUT':
      await ExtensionLogin.logout();
      return { success: true };

    default:
      return { error: 'Unknown message type' };
  }
}

// Notify content scripts when auth changes
ExtensionLogin.onAuthChange((user) => {
  chrome.tabs.query({}, (tabs) => {
    tabs.forEach((tab) => {
      chrome.tabs.sendMessage(tab.id, {
        type: 'AUTH_CHANGED',
        user
      }).catch(() => {});
    });
  });
});

Content Script

javascript
// content/content.js

// Message helper
async function sendMessage(type, data = {}) {
  try {
    return await chrome.runtime.sendMessage({ type, data });
  } catch (error) {
    console.error('Failed to send message:', error);
    return { success: false, error: { message: error.message } };
  }
}

// Auth helpers
async function isAuthenticated() {
  const { authenticated } = await sendMessage('IS_AUTHENTICATED');
  return authenticated;
}

async function getUser() {
  const { user } = await sendMessage('GET_USER');
  return user;
}

// Initialize
async function init() {
  const user = await getUser();

  if (user) {
    showAuthenticatedUI(user);
  } else {
    showUnauthenticatedUI();
  }
}

// Create floating button
function createFloatingButton() {
  const existing = document.getElementById('ext-auth-button');
  if (existing) existing.remove();

  const button = document.createElement('div');
  button.id = 'ext-auth-button';
  button.className = 'ext-floating-button';

  return button;
}

// Show UI for authenticated users
function showAuthenticatedUI(user) {
  const button = createFloatingButton();

  button.innerHTML = `
    <div class="ext-user-badge">
      <img src="${getUserAvatar(user)}" class="ext-avatar" alt="">
      <span class="ext-user-name">${user.name || user.email}</span>
    </div>
  `;

  button.addEventListener('click', () => {
    showUserPanel(user);
  });

  document.body.appendChild(button);
}

// Show UI for unauthenticated users
function showUnauthenticatedUI() {
  const button = createFloatingButton();

  button.innerHTML = `
    <div class="ext-login-prompt">
      <span>Sign In</span>
    </div>
  `;

  button.addEventListener('click', () => {
    showLoginPanel();
  });

  document.body.appendChild(button);
}

// User panel
function showUserPanel(user) {
  removePanel();

  const panel = document.createElement('div');
  panel.id = 'ext-panel';
  panel.className = 'ext-panel';
  panel.innerHTML = `
    <div class="ext-panel-header">
      <img src="${getUserAvatar(user)}" class="ext-panel-avatar" alt="">
      <div>
        <div class="ext-panel-name">${user.name || 'User'}</div>
        <div class="ext-panel-email">${user.email}</div>
      </div>
    </div>
    <div class="ext-panel-actions">
      <button id="ext-logout-btn" class="ext-btn ext-btn-secondary">
        Sign Out
      </button>
    </div>
  `;

  document.body.appendChild(panel);

  // Handle logout
  document.getElementById('ext-logout-btn').addEventListener('click', async () => {
    await sendMessage('LOGOUT');
    removePanel();
    showUnauthenticatedUI();
  });

  // Close on outside click
  document.addEventListener('click', handleOutsideClick);
}

// Login panel
function showLoginPanel() {
  removePanel();

  const panel = document.createElement('div');
  panel.id = 'ext-panel';
  panel.className = 'ext-panel';
  panel.innerHTML = `
    <div class="ext-panel-header">
      <h3>Sign In</h3>
    </div>
    <form id="ext-login-form" class="ext-form">
      <input type="email" id="ext-email" placeholder="Email" required>
      <input type="text" id="ext-name" placeholder="Name (optional)">
      <button type="submit" class="ext-btn ext-btn-primary">
        Continue
      </button>
    </form>
    <div id="ext-error" class="ext-error hidden"></div>
  `;

  document.body.appendChild(panel);

  // Handle form submit
  document.getElementById('ext-login-form').addEventListener('submit', async (e) => {
    e.preventDefault();

    const email = document.getElementById('ext-email').value;
    const name = document.getElementById('ext-name').value;
    const errorEl = document.getElementById('ext-error');

    errorEl.classList.add('hidden');

    const result = await sendMessage('IDENTIFY', { email, name: name || undefined });

    if (result.success) {
      removePanel();
      showAuthenticatedUI(result.user);
    } else {
      errorEl.textContent = result.error.message;
      errorEl.classList.remove('hidden');
    }
  });

  // Close on outside click
  document.addEventListener('click', handleOutsideClick);
}

// Helper functions
function removePanel() {
  const panel = document.getElementById('ext-panel');
  if (panel) panel.remove();
  document.removeEventListener('click', handleOutsideClick);
}

function handleOutsideClick(e) {
  const panel = document.getElementById('ext-panel');
  const button = document.getElementById('ext-auth-button');

  if (panel && !panel.contains(e.target) && !button.contains(e.target)) {
    removePanel();
  }
}

function getUserAvatar(user) {
  if (user.picture) return user.picture;

  const initials = (user.name || user.email)
    .split(' ')
    .map(n => n[0])
    .join('')
    .toUpperCase()
    .slice(0, 2);

  return `https://ui-avatars.com/api/?name=${initials}&background=3b82f6&color=fff`;
}

// Listen for auth changes from background
chrome.runtime.onMessage.addListener((message) => {
  if (message.type === 'AUTH_CHANGED') {
    removePanel();
    if (message.user) {
      showAuthenticatedUI(message.user);
    } else {
      showUnauthenticatedUI();
    }
  }
});

// Start
init();

Content Script CSS

css
/* content/content.css */

.ext-floating-button {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 999999;
  cursor: pointer;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.ext-user-badge,
.ext-login-prompt {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  background: white;
  border-radius: 24px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
  transition: transform 0.2s, box-shadow 0.2s;
}

.ext-user-badge:hover,
.ext-login-prompt:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}

.ext-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
}

.ext-user-name {
  font-size: 14px;
  font-weight: 500;
  color: #1a1a1a;
}

.ext-login-prompt span {
  font-size: 14px;
  font-weight: 500;
  color: #3b82f6;
}

/* Panel */
.ext-panel {
  position: fixed;
  bottom: 70px;
  right: 20px;
  width: 280px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
  z-index: 999999;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.ext-panel-header {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px;
  border-bottom: 1px solid #e5e7eb;
}

.ext-panel-avatar {
  width: 48px;
  height: 48px;
  border-radius: 50%;
}

.ext-panel-name {
  font-size: 16px;
  font-weight: 600;
  color: #1a1a1a;
}

.ext-panel-email {
  font-size: 14px;
  color: #6b7280;
}

.ext-panel-actions {
  padding: 16px;
}

/* Form */
.ext-form {
  padding: 16px;
}

.ext-form input {
  width: 100%;
  padding: 10px 12px;
  margin-bottom: 12px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  font-size: 14px;
}

.ext-form input:focus {
  outline: none;
  border-color: #3b82f6;
}

/* Buttons */
.ext-btn {
  width: 100%;
  padding: 10px 16px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background 0.2s;
}

.ext-btn-primary {
  background: #3b82f6;
  color: white;
}

.ext-btn-primary:hover {
  background: #2563eb;
}

.ext-btn-secondary {
  background: #f3f4f6;
  color: #374151;
}

.ext-btn-secondary:hover {
  background: #e5e7eb;
}

/* Error */
.ext-error {
  margin: 0 16px 16px;
  padding: 10px;
  background: #fee2e2;
  border-radius: 6px;
  color: #991b1b;
  font-size: 14px;
}

.hidden {
  display: none;
}

Conditional Injection

Only show UI on specific sites:

javascript
// content/content.js

const ALLOWED_DOMAINS = [
  'github.com',
  'stackoverflow.com',
  'example.com'
];

function shouldInject() {
  const hostname = window.location.hostname;
  return ALLOWED_DOMAINS.some(domain =>
    hostname === domain || hostname.endsWith('.' + domain)
  );
}

// Only initialize on allowed domains
if (shouldInject()) {
  init();
}

Page-Specific Features

Enable features based on auth status:

javascript
// Enable premium features for authenticated users
async function enableFeatures() {
  const user = await getUser();

  if (user) {
    // Enable premium features
    document.querySelectorAll('.premium-feature').forEach(el => {
      el.classList.remove('disabled');
    });

    // Inject additional functionality
    injectPremiumTools();
  }
}

function injectPremiumTools() {
  // Add premium toolbar, enhanced features, etc.
}

Security Considerations

  1. Don't expose sensitive data: Content scripts run in page context
  2. Validate messages: Always validate messages from web pages
  3. Use background for API calls: Don't make API calls directly from content scripts
javascript
// GOOD: Get user from background
const { user } = await sendMessage('GET_USER');

// BAD: Don't import SDK in content script
// import ExtensionLogin from 'extensionlogin'; // Don't do this

Next Steps

Built for Chrome Extension Developers