A simple and practical SSO implementation with MS Entra ID.
Master SharePoint Access: A Guide to Implementing SSO with Microsoft Entra ID
In the modern enterprise landscape, “password fatigue” is a real productivity killer. For developers building custom web applications that need to pull data from SharePoint, the gold standard for solving this is Single Sign-On (SSO).
By leveraging Microsoft Entra ID (formerly Azure Active Directory), you can create a seamless experience where users log in once and gain secure access to all their SharePoint resources. Here is how Bob (our automation-loving bot) approaches the implementation.
The Big Picture: How it Works

The architecture relies on the OAuth 2.0 Authorization Code Flow with PKCE, ensuring that your web application never actually sees the user’s password. Instead, it handles secure tokens.
> PKCE (Proof Key for Code Exchange) is a security enhancement for OAuth 2.0. It requires the browser’s Web Crypto API, which may not be available in all browsers or contexts.
The 3-Step Operation:
- The Handshake: Your app redirects the user to Microsoft Entra ID.
- The Proof: The user authenticates, and Entra ID sends a temporary “Authorization Code” back to your app.
- The Key: Your app exchanges that code for an Access Token and an ID Token.
Setting Up the “Gatekeeper” (Azure Portal)
Before writing code, you must register your application in the Azure Portal to define what it’s allowed to do. To access SharePoint sites like CollabHome.aspx, your app needs specific "Scopes" or permissions:
| Permission | What it allows |
| -------------------- | --------------------------------------------- |
| **`User.Read`** | Basic profile access (Sign-in) |
| **`Sites.Read.All`** | Read items across SharePoint site collections |
| **`Files.Read.All`** | Access to documents and files |
| **`offline_access`** | Stay signed in via refresh tokens |
Always follow the Principle of Least Privilege. Only request
Files.Read.Allif your application actually needs to download or read document contents.
The Implementation Flow
Once the app is registered, the logic inside your Web Application follows this sequence:
- Challenge: When a user hits a protected page, redirect them to the Entra ID login page.
- Token Exchange: Once they return with a code, use your Client ID and PKCE challenge to get the Access Token.
- The API Call: Attach that Access Token to the header of your request to SharePoint:
Example:
Authorization: Bearer <YOUR_TOKEN>
- Validation: SharePoint validates the token with Entra ID and serves the data (e.g., from
https://xxx.sharepoint.com/...).
Security Must-Haves
Bob doesn’t cut corners on security. When implementing your side of the SSO, keep these in mind:
- Use HTTPS Only: Never exchange tokens over unencrypted connections.
- Secure Storage: Store tokens in httpOnly cookies to protect against Cross-Site Scripting (XSS).
- State Parameter: Always use a state parameter in your auth requests to prevent Cross-Site Request Forgery (CSRF).
Application Side: Registering Your App in the Microsoft Entra Admin Center
To get SSO running for your SharePoint sites, you need to tell Microsoft Entra ID (formerly Azure AD) to trust your application. Here is the exact configuration path Bob recommends:
Create the App Registration
- Sign in to the Microsoft Entra admin center.
- Navigate to
Identity > Applications > App registrationsand select New registration. - Name: Give it a clear name (e.g.,
SharePoint-SSO-App). - Supported account types: Select “Accounts in this organizational directory only” (Single tenant) for internal company use.
- Redirect URI: Select Web and enter your application’s callback URL (e.g., https://myapp.com/auth/callback).
Implementation And Code
To achieve the automated authentication and data retrieval flow detailed in our architecture, the codebase is architected into specialized modules that handle specific stages of the Identity and Resource lifecycle.
Functional Breakdown by Script
Environment Orchestration (env-config.js & env-loader.js): These scripts serve as the foundation, ensuring that sensitive parameters like the Client ID and Tenant ID are loaded and accessible to the application globally.
/**
* env-config.js
* Environment Configuration Template
*
* INSTRUCTIONS:
* 1. Copy this file to scripts/env-config.js
* 2. Replace all placeholder values with your actual configuration
* 3. Never commit scripts/env-config.js to Git (it's in .gitignore)
*
* This file must be loaded BEFORE config.js in index.html
*/
// Environment variables - Replace with your actual values
window.ENV_CONFIG = {
// Azure AD Configuration
CLIENT_ID: 'YOUR_CLIENT_ID_HERE',
TENANT_ID: 'YOUR_TENANT_ID_HERE',
// SharePoint Tenant Information
TENANT_NAME: 'your-tenant-name',
TENANT_DOMAIN: 'your-tenant-name.sharepoint.com',
// SharePoint Site URLs
SITE_A_URL: 'https://your-tenant-name.sharepoint.com/sites/siteA',
SITE_A_PATH: '/sites/siteA',
SITE_A_PAGE: 'https://your-tenant-name.sharepoint.com/sites/siteA/SitePages/Home.aspx',
SITE_B_URL: 'https://your-tenant-name.sharepoint.com/sites/siteB',
SITE_B_PATH: '/sites/siteB',
SITE_B_PAGE: 'https://your-tenant-name.sharepoint.com/sites/siteB/SitePages/Home.aspx',
// Test User Emails (optional - for documentation purposes)
ADMIN_EMAIL: 'admin@your-tenant-name.onmicrosoft.com',
USER1_EMAIL: 'user1@your-tenant-name.onmicrosoft.com',
USER2_EMAIL: 'user2@your-tenant-name.onmicrosoft.com'
};
// Helper function to get environment variable
function getEnv(key, defaultValue = '') {
return window.ENV_CONFIG[key] || defaultValue;
}
console.log('✅ Environment configuration loaded');
/**
* env-loader.js
* Environment Variable Loader
* Loads configuration from .env file
*
* This script fetches and parses the .env file to make environment variables
* available to the application. In production, you would use a proper build
* system or server-side environment variables.
*/
// Environment variables object
const ENV = {};
/**
* Load environment variables from .env file
* @returns {Promise<Object>} Environment variables object
*/
async function loadEnv() {
try {
// Fetch .env file
const response = await fetch('.env');
if (!response.ok) {
console.warn('⚠️ .env file not found. Using default configuration.');
console.warn('📝 Copy .env.example to .env and configure your settings.');
return ENV;
}
const text = await response.text();
// Parse .env file
const lines = text.split('\n');
for (const line of lines) {
// Skip empty lines and comments
if (!line.trim() || line.trim().startsWith('#')) {
continue;
}
// Parse KEY=VALUE format
const match = line.match(/^([^=]+)=(.*)$/);
if (match) {
const key = match[1].trim();
let value = match[2].trim();
// Remove quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
ENV[key] = value;
}
}
console.log('✅ Environment variables loaded successfully');
return ENV;
} catch (error) {
console.error('❌ Error loading .env file:', error);
console.warn('📝 Using default configuration. Copy .env.example to .env and configure your settings.');
return ENV;
}
}
/**
* Get environment variable value
* @param {string} key - Environment variable key
* @param {string} defaultValue - Default value if key not found
* @returns {string} Environment variable value
*/
function getEnv(key, defaultValue = '') {
return ENV[key] || defaultValue;
}
/**
* Check if environment variables are loaded
* @returns {boolean} True if loaded
*/
function isEnvLoaded() {
return Object.keys(ENV).length > 0;
}
/**
* Get all environment variables
* @returns {Object} All environment variables
*/
function getAllEnv() {
return { ...ENV };
}
// Export functions
// In vanilla JS with script tags, these are available globally
// Made with Bob
-
Authentication & Token Management (
auth.js): Utilizing the MSAL 1.x library, this module executes the Implicit Flow to handle user sign-in, redirect callbacks, and the silent acquisition of access tokens.
/**
* auth.js
* Authentication Module
* Handles all Microsoft Entra ID (Azure AD) authentication operations using MSAL.js 1.x
* MSAL 1.x uses implicit flow by default - no PKCE required!
*/
// MSAL instance - will be initialized on page load
let msalInstance = null;
// Current user account
let currentAccount = null;
/**
* Initialize MSAL instance
* Must be called before any other authentication operations
*/
async function initializeMSAL() {
try {
console.log('Initializing MSAL 1.x (Implicit Flow)...');
// Check if MSAL is loaded
if (typeof Msal === 'undefined') {
console.error('MSAL library not loaded!');
throw new Error('MSAL library not loaded. Check your internet connection.');
}
console.log('MSAL library loaded successfully');
// Create MSAL instance with configuration from config.js
// MSAL 1.x uses UserAgentApplication
msalInstance = new Msal.UserAgentApplication(msalConfig);
console.log('MSAL instance created successfully');
// Set up callback for redirect
msalInstance.handleRedirectCallback((error, response) => {
console.log('Redirect callback triggered');
if (error) {
console.error('Redirect callback error:', error);
handleAuthenticationError(error);
hideLoading();
return;
}
if (response) {
console.log('Redirect response received:', response);
currentAccount = response.account;
handleAuthenticationResponse(response);
onSignInSuccess();
} else {
console.log('Redirect callback: no response');
hideLoading();
}
});
console.log('Redirect callback registered');
// Check if there's already a signed-in account
const account = msalInstance.getAccount();
if (account) {
console.log('Found cached account:', account);
currentAccount = account;
// User is already signed in
await onSignInSuccess();
} else {
console.log('No cached account found - sign in required');
onSignInRequired();
}
console.log('MSAL initialized successfully');
return true;
} catch (error) {
console.error('Error initializing MSAL:', error);
handleAuthenticationError(error);
hideLoading();
return false;
}
}
/**
* Sign in using redirect
* MSAL 1.x uses implicit flow by default - no PKCE!
*/
async function signInPopup() {
try {
console.log('Starting sign in with redirect (Implicit Flow)...');
showLoading('Redirecting to sign in...');
// MSAL 1.x loginRedirect uses implicit flow automatically
msalInstance.loginRedirect(loginRequest);
// Note: Code after loginRedirect won't execute as page will redirect
} catch (error) {
console.error('Sign in error:', error);
hideLoading();
handleAuthenticationError(error);
throw error;
}
}
/**
* Sign in using redirect (alias for signInPopup for compatibility)
*/
async function signInRedirect() {
return signInPopup();
}
/**
* Sign out the current user
*/
async function signOut() {
try {
console.log('Signing out...');
showLoading('Signing out...');
// Clear current account
currentAccount = null;
// Sign out using MSAL 1.x
msalInstance.logout();
console.log('Sign out successful');
hideLoading();
onSignOutSuccess();
} catch (error) {
console.error('Sign out error:', error);
hideLoading();
handleAuthenticationError(error);
}
}
/**
* Acquire access token silently (from cache or using refresh token)
* This is the preferred method for getting tokens
*/
async function acquireTokenSilent(scopes = null) {
try {
if (!currentAccount) {
throw new Error('No account signed in');
}
const request = {
scopes: scopes || tokenRequest.scopes,
account: currentAccount
};
console.log('Acquiring token silently...');
const response = await msalInstance.acquireTokenSilent(request);
console.log('Token acquired silently');
return response.accessToken;
} catch (error) {
console.warn('Silent token acquisition failed:', error);
// If silent acquisition fails, try interactive method
if (error.name === 'InteractionRequiredAuthError' ||
error.errorCode === 'consent_required' ||
error.errorCode === 'interaction_required' ||
error.errorCode === 'login_required') {
console.log('Interaction required, falling back to redirect...');
return await acquireTokenRedirect(scopes);
}
throw error;
}
}
/**
* Acquire access token using redirect
* Used when silent acquisition fails
*/
async function acquireTokenRedirect(scopes = null) {
try {
const request = {
scopes: scopes || tokenRequest.scopes,
account: currentAccount
};
console.log('Acquiring token with redirect...');
showLoading('Requesting permissions...');
// This will redirect the page
msalInstance.acquireTokenRedirect(request);
// Code after this won't execute
} catch (error) {
console.error('Token acquisition error:', error);
hideLoading();
handleAuthenticationError(error);
throw error;
}
}
/**
* Acquire access token using popup (for compatibility)
*/
async function acquireTokenPopup(scopes = null) {
return acquireTokenRedirect(scopes);
}
/**
* Get the current signed-in account
*/
function getCurrentAccount() {
return currentAccount;
}
/**
* Check if user is signed in
*/
function isSignedIn() {
return currentAccount !== null;
}
/**
* Get user's display name
*/
function getUserDisplayName() {
if (!currentAccount) return null;
return currentAccount.name || currentAccount.userName;
}
/**
* Get user's email
*/
function getUserEmail() {
if (!currentAccount) return null;
return currentAccount.userName;
}
/**
* Get user's unique identifier
*/
function getUserId() {
if (!currentAccount) return null;
return currentAccount.homeAccountIdentifier;
}
/**
* Handle successful authentication response
*/
function handleAuthenticationResponse(response) {
console.log('Authentication response:', {
account: response.account.userName,
scopes: response.scopes,
tokenType: response.tokenType
});
// Store authentication timestamp
sessionStorage.setItem('authTimestamp', new Date().toISOString());
// Log successful authentication
logAuthenticationEvent('sign_in_success', {
user: response.account.userName,
method: response.fromCache ? 'cache' : 'interactive'
});
}
/**
* Handle authentication errors
*/
function handleAuthenticationError(error) {
console.error('Authentication error:', error);
// Always hide loading indicator on error
hideLoading();
let errorMessage = 'An authentication error occurred.';
let errorDetails = '';
// Parse MSAL errors
if (error && error.errorCode) {
errorMessage = error.errorMessage || error.message;
errorDetails = error.errorCode;
// Handle specific error codes
switch (error.errorCode) {
case 'user_cancelled':
errorMessage = 'Sign in was cancelled.';
break;
case 'consent_required':
errorMessage = 'Additional permissions are required.';
break;
case 'interaction_required':
errorMessage = 'User interaction is required.';
break;
case 'login_required':
errorMessage = 'Please sign in to continue.';
break;
case 'token_renewal_error':
errorMessage = 'Failed to refresh access token. Please sign in again.';
break;
}
}
// Log error
logAuthenticationEvent('auth_error', {
error: errorDetails,
message: errorMessage
});
// Show error to user
showError(errorMessage, errorDetails);
}
/**
* Log authentication events
*/
function logAuthenticationEvent(eventType, data) {
const logEntry = {
timestamp: new Date().toISOString(),
event: eventType,
data: data
};
console.log('Auth Event:', logEntry);
// In production, send to logging service
// Example: sendToLoggingService(logEntry);
}
/**
* Callback when sign in is required
*/
function onSignInRequired() {
console.log('Sign in required');
hideLoading();
updateUIForSignedOut();
}
/**
* Callback when sign in is successful
*/
async function onSignInSuccess() {
console.log('Sign in successful');
updateUIForSignedIn();
// Load user profile
await loadUserProfile();
// Load SharePoint data
await loadSharePointData();
}
/**
* Callback when sign out is successful
*/
function onSignOutSuccess() {
console.log('Sign out successful');
updateUIForSignedOut();
clearUserData();
}
/**
* Validate token expiration
* Returns true if token is still valid
*/
function isTokenValid(expiresOn) {
if (!expiresOn) return false;
const now = new Date();
const expiration = new Date(expiresOn);
// Add 5 minute buffer
const bufferTime = 5 * 60 * 1000;
return expiration.getTime() - now.getTime() > bufferTime;
}
/**
* Get token expiration time
*/
function getTokenExpiration() {
if (!currentAccount) return null;
// MSAL 1.x handles this internally
return null;
}
/**
* Force token refresh
*/
async function forceTokenRefresh() {
try {
console.log('Forcing token refresh...');
const request = {
scopes: tokenRequest.scopes,
account: currentAccount,
forceRefresh: true
};
const response = await msalInstance.acquireTokenSilent(request);
console.log('Token refreshed successfully');
return response.accessToken;
} catch (error) {
console.error('Token refresh error:', error);
throw error;
}
}
// Initialize MSAL when script loads
console.log('Auth module loaded (MSAL 1.x - Implicit Flow)');
// Made with Bob
- *Application Configuration *(
config.js): This central hub defines the security parameters, including the API scopes (e.g.,Sites.Read.All) and the specific endpoints for Microsoft Graph and SharePoint.
/**
* config.js
* Microsoft Entra ID (Azure AD) Configuration
*
* This configuration loads sensitive values from the .env file.
* Copy .env.example to .env and fill in your actual values.
*
* Follow the setup guide in Docs/02-Azure-Setup-Guide.md to get these values
*/
// MSAL 1.x Configuration (Implicit Flow - No PKCE)
const msalConfig = {
auth: {
// Application (client) ID from .env file
// Fallback to placeholder if .env not loaded
clientId: getEnv('CLIENT_ID', 'YOUR_CLIENT_ID_HERE'),
// Directory (tenant) ID from .env file
// Fallback to placeholder if .env not loaded
authority: `https://login.microsoftonline.com/${getEnv('TENANT_ID', 'YOUR_TENANT_ID_HERE')}`,
// Redirect URI - must match Azure Portal configuration
redirectUri: 'http://localhost:3000',
// Post logout redirect URI
postLogoutRedirectUri: 'http://localhost:3000',
// Navigate to login request URL after logout
navigateToLoginRequestUrl: false
},
cache: {
// Cache location: "localStorage" or "sessionStorage"
cacheLocation: 'sessionStorage',
// Store auth state in cookie for IE11 compatibility
storeAuthStateInCookie: false
},
// No system/logger configuration - MSAL 1.x will use defaults
};
/**
* Scopes (permissions) requested from Microsoft Graph API
* These must be configured in Azure Portal under API permissions
*/
// Login request configuration for MSAL 1.x
const loginRequest = {
scopes: [
'User.Read', // Read user profile
'Sites.Read.All', // Read all site collections
'Files.Read.All' // Read all files user can access
],
prompt: 'select_account' // Prompt user to select account
};
/**
* Scopes for acquiring tokens silently
* Used when making API calls after initial login
*/
const tokenRequest = {
scopes: [
'User.Read',
'Sites.Read.All',
'Files.Read.All'
],
forceRefresh: false // Set to true to skip cache and force token refresh
};
/**
* Microsoft Graph API endpoints
*/
const graphConfig = {
// Base URL for Microsoft Graph API
graphMeEndpoint: 'https://graph.microsoft.com/v1.0/me',
// Get user's photo
graphPhotoEndpoint: 'https://graph.microsoft.com/v1.0/me/photo/$value',
// Get SharePoint sites
graphSitesEndpoint: 'https://graph.microsoft.com/v1.0/sites',
// Get site by path
// Usage: graphSiteByPathEndpoint.replace('{hostname}', 'tenant.sharepoint.com').replace('{site-path}', '/sites/sitename')
graphSiteByPathEndpoint: 'https://graph.microsoft.com/v1.0/sites/{hostname}:/{site-path}',
// Get site lists
// Usage: graphSiteListsEndpoint.replace('{site-id}', 'actual-site-id')
graphSiteListsEndpoint: 'https://graph.microsoft.com/v1.0/sites/{site-id}/lists',
// Get list items
// Usage: graphListItemsEndpoint.replace('{site-id}', 'id').replace('{list-id}', 'id')
graphListItemsEndpoint: 'https://graph.microsoft.com/v1.0/sites/{site-id}/lists/{list-id}/items'
};
/**
* SharePoint site configurations
* Loads from .env file with fallback to default values
*/
const sharepointConfig = {
// Tenant information from .env
tenant: getEnv('TENANT_NAME', 'contoso'),
tenantDomain: getEnv('TENANT_DOMAIN', 'contoso.sharepoint.com'),
// SharePoint sites from .env
sites: [
{
name: 'Site A',
url: getEnv('SITE_A_URL', 'https://contoso.sharepoint.com/sites/siteA'),
siteRelativePath: getEnv('SITE_A_PATH', '/sites/siteA'),
pageUrl: getEnv('SITE_A_PAGE', 'https://contoso.sharepoint.com/sites/siteA/SitePages/Home.aspx'),
description: 'Primary collaboration site'
},
{
name: 'Site B',
url: getEnv('SITE_B_URL', 'https://contoso.sharepoint.com/sites/siteB'),
siteRelativePath: getEnv('SITE_B_PATH', '/sites/siteB'),
pageUrl: getEnv('SITE_B_PAGE', 'https://contoso.sharepoint.com/sites/siteB/SitePages/Home.aspx'),
description: 'Secondary collaboration site'
}
],
// SharePoint REST API base URL
// Usage: restApiBase + site.siteRelativePath + '/_api/web'
restApiBase: `https://${getEnv('TENANT_DOMAIN', 'contoso.sharepoint.com')}`
};
/**
* Test users for the application
* Loads from .env file with fallback to default values
*/
const testUsers = [
{
email: getEnv('ADMIN_EMAIL', 'admin@contoso.onmicrosoft.com'),
role: 'Administrator',
description: 'Admin user with full access'
},
{
email: getEnv('USER1_EMAIL', 'user1@contoso.onmicrosoft.com'),
role: 'User',
description: 'Standard user'
},
{
email: getEnv('USER2_EMAIL', 'user2@contoso.onmicrosoft.com'),
role: 'User',
description: 'Standard user'
}
];
/**
* Application settings
*/
const appConfig = {
// Application name
appName: 'SharePoint SSO Demo',
// Application version
version: '1.0.0',
// Enable debug mode (shows more console logs)
debugMode: true,
// Auto-refresh interval for SharePoint data (in milliseconds)
// Set to 0 to disable auto-refresh
autoRefreshInterval: 0, // 5 minutes = 300000
// Maximum number of items to display per list
maxItemsPerList: 10,
// Show detailed error messages to users
showDetailedErrors: true
};
/**
* UI Configuration
*/
const uiConfig = {
// Theme colors
colors: {
primary: '#0078d4', // Microsoft blue
secondary: '#036c70', // SharePoint teal
success: '#107c10', // Green
error: '#d13438', // Red
warning: '#ffa500', // Orange
info: '#0078d4' // Blue
},
// Animation durations (in milliseconds)
animations: {
fadeIn: 300,
fadeOut: 200,
slideIn: 400
},
// Loading messages
loadingMessages: {
authenticating: 'Authenticating with Microsoft Entra ID...',
loadingProfile: 'Loading user profile...',
loadingSharePoint: 'Loading SharePoint data...',
fetchingSites: 'Fetching site information...'
}
};
// Export configuration objects for use in other scripts
// Note: In a module system, you would use: export { msalConfig, loginRequest, ... }
// For vanilla JS with script tags, these are available globally
// Made with Bob
-
SharePoint Integration & Data Fetching (
sharepoint.js): This script acts as the final link, using the acquired tokens to perform authenticated requests against the SharePoint REST API to retrieve site metadata, lists, and document files.
/**
* sharepoint.js
* SharePoint Integration Module
* Handles all SharePoint API calls using Microsoft Graph API and SharePoint REST API
*/
/**
* Call Microsoft Graph API
* Generic function to make authenticated API calls to Microsoft Graph
*/
async function callMSGraph(endpoint, method = 'GET', body = null) {
try {
// Get access token
const token = await acquireTokenSilent();
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
const options = {
method: method,
headers: headers
};
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
options.body = JSON.stringify(body);
}
console.log(`Calling Graph API: ${method} ${endpoint}`);
const response = await fetch(endpoint, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Graph API error: ${response.status} - ${errorText}`);
}
// Handle empty responses
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
console.error('Graph API call failed:', error);
throw error;
}
}
/**
* Call SharePoint REST API
* Generic function to make authenticated API calls to SharePoint REST API
*/
async function callSharePointAPI(endpoint, method = 'GET', body = null) {
try {
// Get access token with SharePoint scope
const token = await acquireTokenSilent(['https://3w2lyf.sharepoint.com/.default']);
const headers = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json;odata=verbose',
'Content-Type': 'application/json;odata=verbose'
};
const options = {
method: method,
headers: headers
};
if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
options.body = JSON.stringify(body);
}
console.log(`Calling SharePoint API: ${method} ${endpoint}`);
const response = await fetch(endpoint, options);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`SharePoint API error: ${response.status} - ${errorText}`);
}
return await response.json();
} catch (error) {
console.error('SharePoint API call failed:', error);
throw error;
}
}
/**
* Get user profile from Microsoft Graph
*/
async function getUserProfile() {
try {
console.log('Fetching user profile...');
const profile = await callMSGraph(graphConfig.graphMeEndpoint);
console.log('User profile retrieved:', profile);
return profile;
} catch (error) {
console.error('Failed to get user profile:', error);
throw error;
}
}
/**
* Get user's photo from Microsoft Graph
*/
async function getUserPhoto() {
try {
console.log('Fetching user photo...');
const token = await acquireTokenSilent();
const response = await fetch(graphConfig.graphPhotoEndpoint, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
console.log('No user photo available');
return null;
}
const blob = await response.blob();
const photoUrl = URL.createObjectURL(blob);
console.log('User photo retrieved');
return photoUrl;
} catch (error) {
console.warn('Failed to get user photo:', error);
return null;
}
}
/**
* Get SharePoint site information using Microsoft Graph
*/
async function getSiteInfo(siteConfig) {
try {
console.log(`Fetching site info for: ${siteConfig.name}`);
// Construct the Graph API endpoint for the site
const endpoint = graphConfig.graphSiteByPathEndpoint
.replace('{hostname}', sharepointConfig.tenantDomain)
.replace('{site-path}', siteConfig.siteRelativePath);
const siteInfo = await callMSGraph(endpoint);
console.log('Site info retrieved:', siteInfo);
return {
id: siteInfo.id,
name: siteInfo.displayName || siteConfig.name,
description: siteInfo.description || siteConfig.description,
url: siteInfo.webUrl,
createdDateTime: siteInfo.createdDateTime,
lastModifiedDateTime: siteInfo.lastModifiedDateTime
};
} catch (error) {
console.error(`Failed to get site info for ${siteConfig.name}:`, error);
throw error;
}
}
/**
* Get all configured SharePoint sites information
*/
async function getAllSitesInfo() {
try {
console.log('Fetching all sites information...');
const sitesPromises = sharepointConfig.sites.map(site =>
getSiteInfo(site).catch(error => ({
error: true,
message: error.message,
name: site.name
}))
);
const sitesInfo = await Promise.all(sitesPromises);
console.log('All sites info retrieved');
return sitesInfo;
} catch (error) {
console.error('Failed to get all sites info:', error);
throw error;
}
}
/**
* Get lists from a SharePoint site
*/
async function getSiteLists(siteId) {
try {
console.log(`Fetching lists for site: ${siteId}`);
const endpoint = graphConfig.graphSiteListsEndpoint.replace('{site-id}', siteId);
const response = await callMSGraph(endpoint);
const lists = response.value || [];
console.log(`Retrieved ${lists.length} lists`);
return lists.map(list => ({
id: list.id,
name: list.displayName,
description: list.description,
createdDateTime: list.createdDateTime,
lastModifiedDateTime: list.lastModifiedDateTime,
webUrl: list.webUrl,
listTemplate: list.list?.template
}));
} catch (error) {
console.error('Failed to get site lists:', error);
throw error;
}
}
/**
* Get items from a SharePoint list
*/
async function getListItems(siteId, listId, top = 10) {
try {
console.log(`Fetching items from list: ${listId}`);
const endpoint = graphConfig.graphListItemsEndpoint
.replace('{site-id}', siteId)
.replace('{list-id}', listId) +
`?$top=${top}&$expand=fields`;
const response = await callMSGraph(endpoint);
const items = response.value || [];
console.log(`Retrieved ${items.length} items`);
return items.map(item => ({
id: item.id,
fields: item.fields,
createdDateTime: item.createdDateTime,
lastModifiedDateTime: item.lastModifiedDateTime,
webUrl: item.webUrl
}));
} catch (error) {
console.error('Failed to get list items:', error);
throw error;
}
}
/**
* Get SharePoint site using REST API
*/
async function getSiteInfoREST(siteConfig) {
try {
console.log(`Fetching site info via REST API: ${siteConfig.name}`);
const endpoint = `${siteConfig.url}/_api/web`;
const response = await callSharePointAPI(endpoint);
const siteData = response.d;
return {
title: siteData.Title,
description: siteData.Description,
url: siteData.Url,
created: siteData.Created,
lastModified: siteData.LastItemModifiedDate,
language: siteData.Language,
serverRelativeUrl: siteData.ServerRelativeUrl
};
} catch (error) {
console.error(`Failed to get site info via REST for ${siteConfig.name}:`, error);
throw error;
}
}
/**
* Get lists from SharePoint site using REST API
*/
async function getListsREST(siteConfig) {
try {
console.log(`Fetching lists via REST API: ${siteConfig.name}`);
const endpoint = `${siteConfig.url}/_api/web/lists`;
const response = await callSharePointAPI(endpoint);
const lists = response.d.results || [];
return lists
.filter(list => !list.Hidden) // Filter out hidden lists
.map(list => ({
id: list.Id,
title: list.Title,
description: list.Description,
itemCount: list.ItemCount,
created: list.Created,
lastModified: list.LastItemModifiedDate,
baseTemplate: list.BaseTemplate
}));
} catch (error) {
console.error('Failed to get lists via REST:', error);
throw error;
}
}
/**
* Get current user's permissions on a site
*/
async function getUserPermissions(siteConfig) {
try {
console.log(`Checking user permissions for: ${siteConfig.name}`);
const endpoint = `${siteConfig.url}/_api/web/currentuser`;
const response = await callSharePointAPI(endpoint);
const user = response.d;
return {
id: user.Id,
title: user.Title,
email: user.Email,
loginName: user.LoginName,
isSiteAdmin: user.IsSiteAdmin
};
} catch (error) {
console.error('Failed to get user permissions:', error);
throw error;
}
}
/**
* Search SharePoint sites
*/
async function searchSharePoint(query, siteConfig) {
try {
console.log(`Searching SharePoint: ${query}`);
const endpoint = `${siteConfig.url}/_api/search/query?querytext='${encodeURIComponent(query)}'`;
const response = await callSharePointAPI(endpoint);
const results = response.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results || [];
return results.map(result => {
const cells = result.Cells.results;
const resultObj = {};
cells.forEach(cell => {
resultObj[cell.Key] = cell.Value;
});
return resultObj;
});
} catch (error) {
console.error('Search failed:', error);
throw error;
}
}
/**
* Get document library files
*/
async function getDocumentLibraryFiles(siteConfig, libraryName = 'Documents') {
try {
console.log(`Fetching files from ${libraryName}...`);
const endpoint = `${siteConfig.url}/_api/web/lists/getbytitle('${libraryName}')/items?$top=10`;
const response = await callSharePointAPI(endpoint);
const files = response.d.results || [];
return files.map(file => ({
id: file.Id,
title: file.Title,
fileName: file.FileLeafRef,
fileType: file.File_x0020_Type,
created: file.Created,
modified: file.Modified,
author: file.Author,
editor: file.Editor
}));
} catch (error) {
console.error('Failed to get document library files:', error);
throw error;
}
}
/**
* Test SharePoint connectivity
* Attempts to connect to all configured sites and returns status
*/
async function testSharePointConnectivity() {
console.log('Testing SharePoint connectivity...');
const results = [];
for (const site of sharepointConfig.sites) {
try {
const startTime = Date.now();
await getSiteInfo(site);
const endTime = Date.now();
results.push({
site: site.name,
status: 'success',
responseTime: endTime - startTime,
message: 'Connected successfully'
});
} catch (error) {
results.push({
site: site.name,
status: 'error',
error: error.message,
message: 'Connection failed'
});
}
}
console.log('Connectivity test results:', results);
return results;
}
/**
* Get comprehensive site data
* Fetches site info, lists, and user permissions
*/
async function getComprehensiveSiteData(siteConfig) {
try {
console.log(`Fetching comprehensive data for: ${siteConfig.name}`);
const [siteInfo, lists, userPerms] = await Promise.allSettled([
getSiteInfo(siteConfig),
getSiteLists(siteConfig.id).catch(() => []),
getUserPermissions(siteConfig).catch(() => null)
]);
return {
site: siteInfo.status === 'fulfilled' ? siteInfo.value : null,
lists: lists.status === 'fulfilled' ? lists.value : [],
userPermissions: userPerms.status === 'fulfilled' ? userPerms.value : null,
error: siteInfo.status === 'rejected' ? siteInfo.reason : null
};
} catch (error) {
console.error('Failed to get comprehensive site data:', error);
throw error;
}
}
/**
* Format SharePoint date
*/
function formatSharePointDate(dateString) {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
/**
* Get site icon based on template
*/
function getSiteIcon(template) {
const icons = {
'DocumentLibrary': '📄',
'PictureLibrary': '🖼️',
'GenericList': '📋',
'Calendar': '📅',
'Tasks': '✅',
'Announcements': '📢',
'Contacts': '👥',
'Links': '🔗'
};
return icons[template] || '📁';
}
console.log('SharePoint module loaded');
// Made with Bob
- All the configurations could be set in an
.envfile so that nothing is hard-coded!
# Microsoft Entra ID Configuration
# Copy this file to .env and fill in your actual values
# Azure AD Application (Client) ID
# Get this from Azure Portal > App registrations > Your app > Overview
CLIENT_ID=12345678-1234-1234-1234-123456789abc
# Azure AD Directory (Tenant) ID
# Get this from Azure Portal > App registrations > Your app > Overview
TENANT_ID=87654321-4321-4321-4321-cba987654321
# SharePoint Tenant Information
TENANT_NAME=your-tenant-name
TENANT_DOMAIN=your-tenant-name.sharepoint.com
# SharePoint Site URLs
SITE_A_URL=https://your-tenant-name.sharepoint.com/sites/siteA
SITE_A_PATH=/sites/siteA
SITE_A_PAGE=https://your-tenant-name.sharepoint.com/sites/siteA/SitePages/Home.aspx
SITE_B_URL=https://your-tenant-name.sharepoint.com/sites/siteB
SITE_B_PATH=/sites/siteB
SITE_B_PAGE=https://your-tenant-name.sharepoint.com/sites/siteB/SitePages/Home.aspx
# Test User Emails (optional - for documentation purposes)
ADMIN_EMAIL=admin@your-tenant-name.onmicrosoft.com
USER1_EMAIL=user1@your-tenant-name.onmicrosoft.com
USER2_EMAIL=user2@your-tenant-name.onmicrosoft.com
- The results 👇
- As we can see, each user view the sites to which they have access!
Conclusion
In conclusion, this project serves as a comprehensive blueprint for modern identity management, successfully demonstrating how to implement Single Sign-On (SSO) via Microsoft Entra ID within a hypothetical enterprise application. By leveraging a modular architecture — spanning from environment orchestration in env-config.js to secure token handling in auth.js and resource access in sharepoint.js—we have established a robust framework that eliminates password fatigue and centralizes security.
Most notably, the efficiency of this approach is highlighted by Bob, who successfully architected, configured, and deployed this entire automated pipeline in under two hours. This rapid turnaround proves that by using standardized libraries like MSAL.js and following a “least privilege” permission model, developers can deliver secure, SharePoint-integrated solutions with remarkable speed and reliability.
>>> Thanks for reading <<<
Links
- Code repository: https://github.com/aairom/ms-sso
- OAuth 2.0 and OIDC authentication flow in the Microsoft identity platform: OAuth 2.0 and OpenID Connect
- What is single sign-on in Microsoft Entra ID?: Single Sign-On Concepts
- Microsoft identity platform documentation: Microsoft Identity Platform
- IBM Project Bob: https://www.ibm.com/products/bob







Top comments (0)