Appearance
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.jsonManifest 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
- Don't expose sensitive data: Content scripts run in page context
- Validate messages: Always validate messages from web pages
- 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 thisNext Steps
- Background Script - Handle auth in background
- Basic Integration - Simple popup example
- Security - Content script security