From 3f2d855e293f2d20ae90f1fcac3ce84c9dc9d778 Mon Sep 17 00:00:00 2001 From: Himanshi Agrawal Date: Tue, 12 Nov 2024 11:53:43 +0530 Subject: [PATCH 1/7] Us10328 implement --- .../Microsoft.GS.DPS.Host/Program.cs | 20 ++++++ App/frontend-app/src/components/App.jsx | 67 ++++++++++++++++++ .../src/providers/TokenProvider.jsx | 55 +++++++++++++++ .../src/providers/UserContext.jsx | 68 +++++++++++++++++++ App/frontend-app/src/utils/authConfig.js | 31 +++++++++ 5 files changed, 241 insertions(+) create mode 100644 App/frontend-app/src/components/App.jsx create mode 100644 App/frontend-app/src/providers/TokenProvider.jsx create mode 100644 App/frontend-app/src/providers/UserContext.jsx create mode 100644 App/frontend-app/src/utils/authConfig.js diff --git a/App/backend-api/Microsoft.GS.DPS.Host/Program.cs b/App/backend-api/Microsoft.GS.DPS.Host/Program.cs index 15734404..6de7b80b 100644 --- a/App/backend-api/Microsoft.GS.DPS.Host/Program.cs +++ b/App/backend-api/Microsoft.GS.DPS.Host/Program.cs @@ -10,6 +10,24 @@ var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + + .AddMicrosoftIdentityWebApi(options => + + { + + configuration.Bind("AzureAd", options); + + options.EventsType = typeof(CustomJwtBearerEvents); + + }, options => + + { + + configuration.Bind("AzureAd", options); + + }); + // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -68,6 +86,8 @@ //} app.UseCors("AllowAll"); +app.UseAuthentication(); +app.UseAuthorization(); app.UseHttpsRedirection(); app.Run(); diff --git a/App/frontend-app/src/components/App.jsx b/App/frontend-app/src/components/App.jsx new file mode 100644 index 00000000..772071ff --- /dev/null +++ b/App/frontend-app/src/components/App.jsx @@ -0,0 +1,67 @@ +import "../assests/css/App.css"; +// eslint-disable-next-line no-unused-vars +import React, { useEffect } from "react"; +import AppRoutes from "./AppRoutes"; +import { + AuthenticatedTemplate, + UnauthenticatedTemplate, + useMsal, + useIsAuthenticated, +} from "@azure/msal-react"; +import { FluentProvider, webLightTheme } from "@fluentui/react-components"; +import { UserProvider } from "../providers/UserContext"; +import { loginRequest } from "../authConfig"; +import { TokenProvider } from "../providers/TokenProvider"; +import classNames from "classnames"; +import { useTranslation } from "react-i18next"; + +const App = () => { + const { instance } = useMsal(); + const isAuthenticated = useIsAuthenticated(); + const { i18n } = useTranslation(); + useEffect(() => { + const handleRedirect = async () => { + try { + await instance.initialize(); + const response = await instance.handleRedirectPromise(); + if (response !== null) { + const account = response.account; + instance.setActiveAccount(account); + } else { + const currentAccount = instance.getActiveAccount(); + if (!currentAccount) { + await instance.loginRedirect(loginRequest); + } + } + } catch (e) { + console.error(e); + } + }; + handleRedirect(); + }, [instance]); + + return ( +
+ + {isAuthenticated && ( + + + + + + + + )} + + + + Signing you in... + +
+ ); +}; + +export default App; diff --git a/App/frontend-app/src/providers/TokenProvider.jsx b/App/frontend-app/src/providers/TokenProvider.jsx new file mode 100644 index 00000000..8fe25f03 --- /dev/null +++ b/App/frontend-app/src/providers/TokenProvider.jsx @@ -0,0 +1,55 @@ +// contexts/TokenContext.js +// eslint-disable-next-line no-unused-vars +import React, { createContext, useState, useEffect } from 'react'; +import { useMsal } from '@azure/msal-react'; +import { graphTokenRequest, tokenRequest } from '../authConfig'; + +const TokenContext = createContext(); + +// eslint-disable-next-line react/prop-types +const TokenProvider = ({ children }) => { + const { instance, accounts } = useMsal(); + const [accessToken, setAccessToken] = useState(localStorage.getItem('accessToken')); + const [graphToken, setGraphToken] = useState(localStorage.getItem('graphToken')); + + const acquireToken = async () => { + const account = instance.getActiveAccount(); + if (account) { + try { + const tokenResponse = await instance.acquireTokenSilent({ ...tokenRequest, account }); + localStorage.setItem('accessToken', tokenResponse.accessToken); + setAccessToken(tokenResponse.accessToken); + + const graphTokenResponse = await instance.acquireTokenSilent({ + ...graphTokenRequest, + account + }); + localStorage.setItem('graphToken', graphTokenResponse.accessToken); + setGraphToken(graphTokenResponse.accessToken); + } catch (error) { + console.error('Token acquisition failed:', error); + // Handle error (e.g., redirect to login page) + } + } + }; + + useEffect(() => { + const refreshToken = async () => { + await acquireToken(); + }; + + if (accounts.length > 0) { + acquireToken(); + const interval = setInterval(refreshToken, 15 * 60 * 1000); + return () => clearInterval(interval); + } + }, [accounts, instance]); + + return ( + + {children} + + ); +}; + +export { TokenContext, TokenProvider }; diff --git a/App/frontend-app/src/providers/UserContext.jsx b/App/frontend-app/src/providers/UserContext.jsx new file mode 100644 index 00000000..3413315d --- /dev/null +++ b/App/frontend-app/src/providers/UserContext.jsx @@ -0,0 +1,68 @@ +// eslint-disable-next-line no-unused-vars +import React, { createContext, useState, useEffect, useContext } from 'react'; +import { getUserInfo } from '../utils/msGraph'; +import { apiEndPoint } from "../authConfig"; +import { TokenContext } from './TokenProvider'; + +const UserContext = createContext(); + +// eslint-disable-next-line react/prop-types +const UserProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const { accessToken, graphToken } = useContext(TokenContext); + + useEffect(() => { + const fetchUserAndSync = async () => { + if (!accessToken || !graphToken) { + setLoading(false); + return; + } + + try { + const userInfo = await getUserInfo(); + + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + headers.append("Authorization", bearer); + headers.append("Content-Type", 'application/json'); + + const user = { + DisplayName: userInfo.displayName, + Name: userInfo.givenName ? userInfo.givenName : userInfo.displayName, + Region: userInfo.officeLocation ? userInfo.officeLocation : "", + PrincipalName: userInfo.userPrincipalName, + JobRole: userInfo.jobTitle ? userInfo.jobTitle : "" + }; + + const response = await fetch(`${apiEndPoint}/User/SyncUser`, { + method: 'POST', + headers: headers, + body: JSON.stringify(user) + }); + + if (!response.ok) { + throw new Error('Failed to sync user with backend'); + } + + const syncedUser = await response.json(); + const userContext = { ...syncedUser }; + setUser(userContext); + } catch (error) { + console.error('Failed to fetch and sync user info:', error); + } finally { + setLoading(false); + } + }; + + fetchUserAndSync(); + }, [accessToken, graphToken]); + + return ( + + {children} + + ); +}; + +export { UserContext, UserProvider }; diff --git a/App/frontend-app/src/utils/authConfig.js b/App/frontend-app/src/utils/authConfig.js new file mode 100644 index 00000000..42672293 --- /dev/null +++ b/App/frontend-app/src/utils/authConfig.js @@ -0,0 +1,31 @@ +import { PublicClientApplication } from '@azure/msal-browser'; + +export const msalConfig = { + auth: { + clientId: import.meta.env.VITE_MSAL_AUTH_CLIENTID,// "eb24479c-61d0-4bff-8bf0-c86f1c481ea5",//"926d0e34-f19a-4ff7-8187-e2403d10c0e6", + authority: import.meta.env.VITE_MSAL_AUTH_AUTHORITY,// "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", + redirectUri: import.meta.env.VITE_MSAL_AUTH_REDIRECTURI// "https://localhost:5173", + }, + cache: { + cacheLocation: "localStorage", + storeAuthStateInCookie: true, + }, +}; + +export const loginRequest = { + scopes: ["openid", "profile", import.meta.env.VITE_MSAL_LOGIN_SCOPE, "User.Read"] +}; + +export const tokenRequest = { + scopes: [import.meta.env.VITE_MSAL_LOGIN_SCOPE], +}; + +export const msalInstance = new PublicClientApplication(msalConfig); + +export const graphTokenRequest = { + scopes: ["User.Read"] +}; +export const graphConfig = { + graphMeEndpoint: "https://graph.microsoft.com/v1.0/me", +}; +export const apiEndPoint = import.meta.env.VITE_API_URL;// "https://localhost:7230"; From e11788386a8152b82ffc0da13ec7f692a5bef060 Mon Sep 17 00:00:00 2001 From: Bangarraju-Microsoft Date: Thu, 14 Nov 2024 18:31:28 +0530 Subject: [PATCH 2/7] US#10328 authentication implemented --- App/frontend-app/src/App.tsx | 43 ++++++---- .../src/components/headerBar/headerBar.tsx | 83 ++++++++++++------- App/frontend-app/src/main.tsx | 20 ++--- App/frontend-app/src/msaConfig.ts | 33 ++++++++ .../src/providers/AuthWrapper.tsx | 35 ++++++++ .../src/providers/TokenProvider.jsx | 55 ------------ .../src/providers/UserContext.jsx | 68 --------------- .../src/providers/msalInstance.ts | 5 ++ App/frontend-app/src/utils/authConfig.js | 31 ------- 9 files changed, 164 insertions(+), 209 deletions(-) create mode 100644 App/frontend-app/src/msaConfig.ts create mode 100644 App/frontend-app/src/providers/AuthWrapper.tsx delete mode 100644 App/frontend-app/src/providers/TokenProvider.jsx delete mode 100644 App/frontend-app/src/providers/UserContext.jsx create mode 100644 App/frontend-app/src/providers/msalInstance.ts delete mode 100644 App/frontend-app/src/utils/authConfig.js diff --git a/App/frontend-app/src/App.tsx b/App/frontend-app/src/App.tsx index 10d8065c..a6fc3c0a 100644 --- a/App/frontend-app/src/App.tsx +++ b/App/frontend-app/src/App.tsx @@ -11,6 +11,11 @@ import { SnackbarProvider } from "notistack"; import { SnackbarSuccess } from "./components/snackbar/snackbarSuccess"; import { SnackbarError } from "./components/snackbar/snackbarError"; + +import { MsalProvider } from '@azure/msal-react'; +import { msalInstance } from './providers/msalInstance'; +import AuthWrapper from './providers/AuthWrapper'; + /* Application insights initialization */ //const reactPlugin: ReactPlugin = Telemetry.initAppInsights(window.ENV.APP_INSIGHTS_CS, true); @@ -24,23 +29,27 @@ webLightTheme.colorNeutralForeground1 = (fullConfig.theme!.colors as any).black; function App() { return ( - - {/* Removed MsalProvider and MsalAuthenticationTemplate */} - {/* */} - - - - - - - - - - {/* */} - + + + + {/* Removed MsalProvider and MsalAuthenticationTemplate */} + {/* */} + + + + + + + + + + {/* */} + + + ); } diff --git a/App/frontend-app/src/components/headerBar/headerBar.tsx b/App/frontend-app/src/components/headerBar/headerBar.tsx index fe6e9099..7430bec3 100644 --- a/App/frontend-app/src/components/headerBar/headerBar.tsx +++ b/App/frontend-app/src/components/headerBar/headerBar.tsx @@ -1,8 +1,10 @@ import React, { MouseEventHandler, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; -import { useMsal } from "@azure/msal-react"; -import { Auth } from "../../utils/auth/auth"; +//import { useMsal } from "@azure/msal-react"; +//import { Auth } from "../../utils/auth/auth"; + +//import { loginRequest } from "../../msaConfig"; import { RedirectRequest } from "@azure/msal-browser"; import { Avatar, @@ -18,6 +20,12 @@ import resolveConfig from "tailwindcss/resolveConfig"; import TailwindConfig from "../../../tailwind.config"; import { isPlatformAdmin } from "../../utils/auth/roles"; + +import { useMsal, useIsAuthenticated } from "@azure/msal-react"; +import { msalInstance } from "../../providers/msalInstance"; +import { loginRequest } from "../../msaConfig"; +import { AccountInfo } from "@azure/msal-browser"; + const fullConfig = resolveConfig(TailwindConfig); const useStylesAvatar = makeStyles({ root: { @@ -51,14 +59,18 @@ interface NavItem { export function HeaderBar({ location }: { location?: NavLocation }) { const { t } = useTranslation(); const [openDrawer, setOpenDrawer] = useState(false); - const { instance, accounts } = useMsal(); + //const { instance, accounts } = useMsal(); const navigate = useNavigate(); const stylesAvatar = useStylesAvatar(); const linkClasses = "cursor-pointer hover:no-underline hover:border-b-[3px] h-9 min-h-0 block text-white"; const linkCurrent = "pointer-events-none border-b-[3px]"; - const isAuthenticated = accounts.length > 0; - const isAdmin = isPlatformAdmin(accounts); + //const isAuthenticated = accounts.length > 0; + //const isAdmin = isPlatformAdmin(accounts); + + const { accounts, instance } = useMsal(); + const isAuthenticated = useIsAuthenticated(); + const activeAccount: AccountInfo | undefined = accounts[0]; const navItems: (NavItem | null)[] = useMemo( () => [ @@ -79,21 +91,21 @@ export function HeaderBar({ location }: { location?: NavLocation }) { // : null, isAuthenticated ? { - key: "sign-out", - label: t("components.header-bar.sign-out"), - isPrimary: false, - action: signOut, - } - : null, - isAuthenticated - ? { - key: "personalDocuments", - label: "Personal Documents", - isPrimary: false, - location: NavLocation.PersonalDocs, - to: "/personalDocuments", - } + key: "sign-out", + label: t("components.header-bar.sign-out"), + isPrimary: false, + action: signOut, + } : null, + // isAuthenticated + // ? { + // key: "personalDocuments", + // label: "Personal Documents", + // isPrimary: false, + // location: NavLocation.PersonalDocs, + // to: "/personalDocuments", + // } + // : null, ], [accounts] ); @@ -102,13 +114,28 @@ export function HeaderBar({ location }: { location?: NavLocation }) { setOpenDrawer((openDrawer) => !openDrawer); } - function signIn() { - instance.loginRedirect(Auth.getAuthenticationRequest() as RedirectRequest); - } + // function signIn() { + // instance.loginRedirect(Auth.getAuthenticationRequest() as RedirectRequest); + // } - function signOut() { - instance.logoutRedirect(); - } + // function signOut() { + // instance.logoutRedirect(); + // } + + async function signOut() { + if (activeAccount) { + try { + await instance.logoutRedirect({ + account: activeAccount, + onRedirectNavigate: () => false, // Prevent default navigation + }); + } catch (error) { + console.error("Logout failed:", error); + } + } else { + console.warn("No active account found for logout."); + } + }; function renderLink(nav: NavItem, className?: string) { return ( @@ -138,17 +165,17 @@ export function HeaderBar({ location }: { location?: NavLocation }) { <>
- logo + logo
{t("components.header-bar.title")}
- +
{t("components.header-bar.sub-title")}
- +