Implementing Single Sign-On (SSO) Between a Next.js App and Chrome Extension
While developing a Next.js web application alongside a browser extension, our team encountered a significant user login issue during testing. Although the initial development went smoothly, this problem negatively impacted the user experience. This post outlines the challenge we faced, our solution involving single sign-on (SSO), and the process we followed to implement it.
The Problem: Disconnected User Sessions
Users who registered and logged into the web application were required to sign in again when using the browser extension. This redundancy was not only inconvenient but also disrupted the seamless experience we aimed to provide. Recognizing the need for a unified authentication process, we set out to implement an SSO solution to synchronize user sessions across both platforms.
Our Solution: Implementing SSO with NextAuth
To address this issue, we utilized NextAuth to create a single sign-on system. The goal was to allow users to log in once on the web application and maintain that session across both the backend and the browser extension. This approach would eliminate the need for repeated authentication, enhancing the overall user experience.
The Implementation Process
Below is the step-by-step process we followed to implement the SSO solution.
Configuring NextAuth for Unified Authentication
We set up NextAuth to handle authentication for both the web app and the browser extension. The signIn callback was customized to manage user sessions effectively.
signIn: async ({ account, user }) => {
try {
const cookieStore = cookies();
// Check for callbackUrl in production environment
let callbackUrl = cookieStore.get('__Secure-next-auth.callback-url')?.value;
// If no callbackUrl found, check using development cookie key name
if (!callbackUrl) {
callbackUrl = cookieStore.get('next-auth.callback-url')?.value;
}
// Extract and validate the user role
const role = callbackUrl?.split('=').pop()?.toUpperCase() as RolesEnum;
if (!role?.length || !allowedRoles.includes(role)) {
return false;
}
// Validate the user with the backend
const dbUser = await validateUser({
access_token: account?.access_token,
id_token: account?.id_token,
as: role,
});
if (!dbUser.success) {
return false;
}
// Bind additional properties to the user object
user.accessToken = dbUser.data.accessToken;
user.refreshToken = account?.refresh_token || '';
user.id = dbUser.data.user.id;
user.role = dbUser.data.user.role;
user.image = dbUser.data.user.image;
user.createdAt = dbUser.data.user.createdAt;
user.name = dbUser.data.user.name;
user.tokenExpires = account?.expires_at || 0;
user.googleAccessToken = account?.access_token || '';
return true;
} catch (err) {
console.log('Error in signIn callback', { err });
return false;
}
}In this configuration:
- Session Management:
NextAuthhandles the session with Google OAuth, while our backend maintains a session for user authentication. - Role Verification: The user's role is extracted from the callback URL and validated against allowed roles.
- User Validation: The user is validated with the backend using tokens obtained from the authentication provider.
- Session Persistence: User details and tokens are stored to maintain the session across the web app and extension.
Synchronizing Sessions with the Chrome Extension
We implemented a content script in the Chrome extension that interacts with the web application's session storage to synchronize authentication data.
// content-script.js
(function() {
'use strict';
// Function to check and sync session data
function syncSessionData() {
try {
// Access the web app's localStorage
const sessionData = localStorage.getItem('next-auth.session-token');
if (sessionData) {
// Store session data in Chrome extension storage
chrome.storage.local.set({
'session-token': sessionData,
'last-sync': Date.now()
}, function() {
console.log('Session data synchronized with extension');
});
// Retrieve additional user data if available
const userData = localStorage.getItem('user-data');
if (userData) {
chrome.storage.local.set({
'user-data': userData
});
}
}
} catch (error) {
console.error('Error syncing session data:', error);
}
}
// Monitor for changes in localStorage
window.addEventListener('storage', function(e) {
if (e.key === 'next-auth.session-token') {
syncSessionData();
}
});
// Initial sync on page load
syncSessionData();
// Periodic check every 30 seconds
setInterval(syncSessionData, 30000);
})();In this script:
- Session Monitoring: The script periodically checks if the user is logged in by accessing the web app's
localStorage. - Data Sharing: Upon detecting an active session, it stores the session data in the Chrome extension's
chrome.storage.local. - Content Script: This approach leverages the content script's ability to access both the extension's and the web page's storage, enabling seamless data sharing.
Extension Background Script
To utilize the synced session data in the extension, we created a background script:
// background.js
chrome.storage.local.get(['session-token', 'user-data'], function(result) {
if (result['session-token']) {
// User is authenticated
const sessionToken = result['session-token'];
const userData = result['user-data'] ? JSON.parse(result['user-data']) : null;
// Use the session token for API requests
fetch('https://your-api.com/endpoint', {
headers: {
'Authorization': `Bearer ${sessionToken}`,
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
console.log('API call successful:', data);
})
.catch(error => {
console.error('API call failed:', error);
});
}
});
// Listen for storage changes
chrome.storage.onChanged.addListener(function(changes, namespace) {
if (namespace === 'local' && changes['session-token']) {
console.log('Session token updated:', changes['session-token'].newValue);
// Handle session token updates
}
});Maintaining Session Consistency
By sharing the session data between the web app and the extension, we ensured that the user's authentication state remained consistent. This method eliminated the need for the user to log in separately on the extension, providing a unified and smooth user experience.
