spa-entra-id
Version:
Single Page Application with Azure Entra ID authentication using MSAL
73 lines (69 loc) • 12.9 kB
JavaScript
var w=Object.defineProperty;var y=(c,e,t)=>e in c?w(c,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):c[e]=t;var h=(c,e,t)=>y(c,typeof e!="symbol"?e+"":e,t);import{P as m,m as l,l as g,g as p,c as f,a as b}from"./config-DsmLiMCp.js";class I{constructor(){h(this,"msalInstance");this.msalInstance=new m(l)}async initialize(){try{if(!this.validateConfiguration())throw new Error("Configuration validation failed. Please check your environment variables.");await this.msalInstance.initialize(),await this.handleRedirectResponse()}catch(e){throw console.error("MSAL initialization failed:",e),e}}async handleRedirectResponse(){try{const e=await this.msalInstance.handleRedirectPromise();e&&console.log("Redirect login successful:",e)}catch(e){throw console.error("Error handling redirect response:",e),e}}async signIn(e=!1){var t,n;try{if(e){console.log("Using redirect flow as requested..."),await this.msalInstance.loginRedirect(g);return}await this.msalInstance.loginPopup(g),console.log("Login successful")}catch(r){if(console.error("Popup login failed:",r),r&&typeof r=="object"){const o=r;if(o.errorCode==="popup_window_error"||(t=o.errorMessage)!=null&&t.includes("popup")||(n=o.errorMessage)!=null&&n.includes("blocked")||o.name==="BrowserAuthError"){console.log("Popup was blocked, automatically switching to redirect flow..."),sessionStorage.setItem("preferRedirectAuth","true");try{await this.msalInstance.loginRedirect(g);return}catch(s){throw console.error("Redirect login also failed:",s),s}}}throw r}}shouldUseRedirectFlow(){return sessionStorage.getItem("preferRedirectAuth")==="true"}async signOut(){try{await this.msalInstance.logoutPopup()}catch(e){throw console.error("Logout failed:",e),e}}isLoggedIn(){return this.msalInstance.getAllAccounts().length>0}getCurrentAccount(){const e=this.msalInstance.getAllAccounts();return e.length>0?e[0]:null}isRedirectResponse(){return this.msalInstance.getActiveAccount()!==null||window.location.hash.includes("access_token")||window.location.search.includes("code=")}async getAccessToken(){const e=this.getCurrentAccount();if(!e)throw new Error("No account found");try{return(await this.msalInstance.acquireTokenSilent({...p,account:e})).accessToken}catch(t){if(console.error("Silent token acquisition failed:",t),this.shouldUseRedirectFlow())throw console.log("Using redirect flow for token acquisition..."),await this.msalInstance.acquireTokenRedirect(p),new Error("Redirect initiated for token acquisition");try{return(await this.msalInstance.acquireTokenPopup(p)).accessToken}catch(n){throw console.error("Popup token acquisition also failed:",n),console.log("Popup token acquisition failed, trying redirect..."),await this.msalInstance.acquireTokenRedirect(p),new Error("Redirect initiated for token acquisition")}}}async getCustomAudienceToken(){const e=this.getCurrentAccount();if(!e)throw new Error("No account found");try{return(await this.msalInstance.acquireTokenSilent({...f,account:e})).accessToken}catch(t){return console.error("Silent token acquisition failed for custom audience, trying popup:",t),(await this.msalInstance.acquireTokenPopup(f)).accessToken}}async callGraphAPI(){try{const e=await this.getAccessToken(),t=await fetch(b.graphMeEndpoint,{headers:{Authorization:`Bearer ${e}`}});if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);return await t.json()}catch(e){throw console.error("Graph API call failed:",e),e}}decodeJWT(e){try{const t=e.split(".");if(t.length!==3)throw new Error("Invalid JWT format");const n=t[1],r=n+"=".repeat((4-n.length%4)%4),o=atob(r.replace(/-/g,"+").replace(/_/g,"/"));return JSON.parse(o)}catch(t){return console.error("Error decoding JWT:",t),null}}async detectPopupBlocker(){try{const e=window.open("","_blank","width=1,height=1");return!e||e.closed||typeof e.closed>"u"?!0:(e.close(),!1)}catch{return!0}}validateConfiguration(){const e=[];return(!l.auth.clientId||l.auth.clientId==="YOUR_CLIENT_ID")&&e.push("Client ID is missing or not set"),(!l.auth.authority||l.auth.authority.includes("YOUR_TENANT_ID"))&&e.push("Authority/Tenant ID is missing or not set"),l.auth.redirectUri||e.push("Redirect URI is missing"),e.length>0?(console.error("Configuration issues found:",e),console.error("Please check your .env file and ensure all values are set correctly."),!1):!0}}const i=new I;class k{constructor(){h(this,"contentElement");h(this,"containerElement");this.contentElement=document.getElementById("content"),this.containerElement=document.querySelector(".container")}showLoading(e="Loading..."){this.contentElement.innerHTML=`<p class="loading">${e}</p>`}showError(e){this.contentElement.innerHTML=`<div class="error">${e}</div>`}async showSignInButton(){document.body.classList.remove("signed-in"),this.containerElement.classList.remove("full-screen");const e=i.shouldUseRedirectFlow(),t=await i.detectPopupBlocker();let n="";t&&!e&&(n='<p class="warning-text">⚠️ Popup blocker detected. Consider using redirect flow.</p>'),this.contentElement.innerHTML=`
<p>Please sign in to continue</p>
<button id="signInBtn">Sign In with Entra ID</button>
${e?'<p class="info-text">✓ Using redirect flow (popup was blocked previously)</p>':""}
${n}
<div class="auth-options">
<button id="useRedirectBtn" class="secondary-btn">Use Redirect Flow</button>
<small>Use redirect flow if you have popup blockers enabled</small>
</div>
`,document.getElementById("signInBtn").addEventListener("click",()=>this.handleSignIn(e||t)),document.getElementById("useRedirectBtn").addEventListener("click",()=>this.handleSignIn(!0))}showUserProfile(e,t,n){document.body.classList.add("signed-in"),this.containerElement.classList.add("full-screen");const r=t?i.decodeJWT(t):null,o=n?i.decodeJWT(n):null;this.contentElement.innerHTML=`
<div class="full-screen-header">
<h1>Azure Entra ID - Authenticated Session</h1>
<div>
<button id="refreshBtn" style="background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3);">Refresh</button>
<button id="signOutBtn" style="background: #dc3545; margin-left: 0.5rem;">Sign Out</button>
</div>
</div>
<div class="full-screen-content">
<div class="profile-section">
<h2>User Profile</h2>
<div class="user-info">
<h3>Welcome, ${e.displayName||e.name||"User"}!</h3>
<p><strong>Email:</strong> ${e.mail||e.userPrincipalName||"N/A"}</p>
<p><strong>Job Title:</strong> ${e.jobTitle||"N/A"}</p>
<p><strong>Department:</strong> ${e.department||"N/A"}</p>
<p><strong>Office Location:</strong> ${e.officeLocation||"N/A"}</p>
<p><strong>Mobile Phone:</strong> ${e.mobilePhone||"N/A"}</p>
</div>
</div>
<div class="token-section">
<h2>Bearer Tokens</h2>
${t?`
<div class="token-info">
<h4>Graph API Bearer Token</h4>
<div class="token-claims">
<p><strong>Audience:</strong> ${(r==null?void 0:r.aud)||"N/A"}</p>
<p><strong>Issuer:</strong> ${(r==null?void 0:r.iss)||"N/A"}</p>
<p><strong>Expires:</strong> ${r!=null&&r.exp?new Date(r.exp*1e3).toLocaleString():"N/A"}</p>
<p><strong>Scopes:</strong> ${(r==null?void 0:r.scp)||"N/A"}</p>
</div>
<textarea readonly class="token-display">${t}</textarea>
<button id="copyGraphTokenBtn">Copy Graph Token</button>
</div>
`:""}
${n?`
<div class="token-info">
<h4>Custom Audience Bearer Token</h4>
<div class="token-claims">
<p><strong>Audience:</strong> ${(o==null?void 0:o.aud)||"N/A"}</p>
<p><strong>Issuer:</strong> ${(o==null?void 0:o.iss)||"N/A"}</p>
<p><strong>Expires:</strong> ${o!=null&&o.exp?new Date(o.exp*1e3).toLocaleString():"N/A"}</p>
<p><strong>Scopes:</strong> ${(o==null?void 0:o.scp)||"N/A"}</p>
</div>
<textarea readonly class="token-display">${n}</textarea>
<button id="copyCustomTokenBtn">Copy Custom Token</button>
</div>
`:""}
</div>
</div>
`;const s=document.getElementById("refreshBtn"),u=document.getElementById("signOutBtn"),d=document.getElementById("copyGraphTokenBtn"),a=document.getElementById("copyCustomTokenBtn");s.addEventListener("click",this.handleRefreshProfile.bind(this)),u.addEventListener("click",this.handleSignOut.bind(this)),d&&t&&d.addEventListener("click",()=>this.copyTokenToClipboard(t,"copyGraphTokenBtn")),a&&n&&a.addEventListener("click",()=>this.copyTokenToClipboard(n,"copyCustomTokenBtn"))}async handleSignIn(e=!1){var t,n,r;try{this.showLoading("Signing in..."),await i.signIn(e),await this.loadUserProfile()}catch(o){if(console.error("Sign in error:",o),o&&typeof o=="object"&&((t=o.message)!=null&&t.includes("Redirect initiated"))){this.showLoading("Redirecting for authentication...");return}let s="Sign in failed. ";if(o&&typeof o=="object"){const a=o;a.errorCode==="popup_window_error"||(n=a.errorMessage)!=null&&n.includes("popup")||(r=a.errorMessage)!=null&&r.includes("blocked")||a.name==="BrowserAuthError"?s+="Popup was blocked by your browser. The app will automatically try redirect flow next time. Please:<br>1. Allow popups for this site, or<br>2. Use the 'Use Redirect Flow' button below<br>3. Try refreshing the page":a.errorCode==="user_cancelled"?s+="Sign in was cancelled.":a.errorCode==="consent_required"?s+="Additional consent is required. Please try again.":a.errorMessage?s+=a.errorMessage:s+="Please check the browser console for details."}else s+="Please check the browser console for details.";this.contentElement.innerHTML=`
<div class="error">
<h3>Authentication Error</h3>
<p>${s}</p>
<div class="error-actions">
<button id="retryBtn" style="margin-top: 1rem;">Try Again</button>
<button id="useRedirectBtn" class="secondary-btn" style="margin-top: 1rem; margin-left: 0.5rem;">Use Redirect Flow</button>
</div>
</div>
`;const u=document.getElementById("retryBtn");u&&u.addEventListener("click",()=>this.handleSignIn());const d=document.getElementById("useRedirectBtn");d&&d.addEventListener("click",()=>this.handleSignIn(!0)),setTimeout(async()=>await this.showSignInButton(),15e3)}}async handleSignOut(){try{this.showLoading("Signing out..."),await i.signOut(),document.body.classList.remove("signed-in"),this.containerElement.classList.remove("full-screen"),await this.showSignInButton()}catch(e){console.error("Sign out error:",e),this.showError("Sign out failed. Please try again.")}}async handleRefreshProfile(){await this.loadUserProfile()}async loadUserProfile(){try{this.showLoading("Loading profile and tokens...");const e=await i.callGraphAPI(),t=await i.getAccessToken();let n;try{n=await i.getCustomAudienceToken()}catch(r){console.warn("Failed to get custom audience token:",r)}this.showUserProfile(e,t,n)}catch(e){console.error("Profile loading error:",e),this.showError("Failed to load profile. Please try again."),setTimeout(()=>{if(i.isLoggedIn()){const t=i.getCurrentAccount();this.showUserProfile({displayName:(t==null?void 0:t.name)||"User",userPrincipalName:(t==null?void 0:t.username)||"N/A"},void 0,void 0)}else this.showSignInButton()},3e3)}}copyTokenToClipboard(e,t="copyTokenBtn"){navigator.clipboard.writeText(e).then(()=>{const n=document.getElementById(t);if(n){const r=n.textContent;n.textContent="Copied!",n.style.background="#28a745",setTimeout(()=>{n.textContent=r,n.style.background="#0078d4"},2e3)}}).catch(n=>{console.error("Failed to copy token:",n),alert("Failed to copy token to clipboard")})}async initialize(){try{this.showLoading("Initializing..."),await i.initialize(),i.isLoggedIn()?await this.loadUserProfile():await this.showSignInButton()}catch(e){console.error("Initialization error:",e),this.showError("Failed to initialize. Please refresh the page.")}}}const E=new k;document.addEventListener("DOMContentLoaded",async()=>{await E.initialize()});