github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/frontend-service/src/ui/utils/AzureAuthProvider.tsx (about) 1 /*This file is part of kuberpult. 2 3 Kuberpult is free software: you can redistribute it and/or modify 4 it under the terms of the Expat(MIT) License as published by 5 the Free Software Foundation. 6 7 Kuberpult is distributed in the hope that it will be useful, 8 but WITHOUT ANY WARRANTY; without even the implied warranty of 9 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 MIT License for more details. 11 12 You should have received a copy of the MIT License 13 along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>. 14 15 Copyright 2023 freiheit.com*/ 16 import * as React from 'react'; 17 import { BrowserHeaders } from 'browser-headers'; 18 import { Configuration, PublicClientApplication } from '@azure/msal-browser'; 19 import { 20 AuthenticatedTemplate, 21 MsalProvider, 22 UnauthenticatedTemplate, 23 useIsAuthenticated, 24 useMsal, 25 } from '@azure/msal-react'; 26 import { GetFrontendConfigResponse } from '../../api/api'; 27 import { createStore } from 'react-use-sub'; 28 import { grpc } from '@improbable-eng/grpc-web'; 29 import { useFrontendConfig } from './store'; 30 import { AuthenticationResult } from '@azure/msal-common'; 31 import { Spinner } from '../components/Spinner/Spinner'; 32 33 type AzureAuthSubType = { 34 authHeader: grpc.Metadata & { 35 Authorization?: String; 36 }; 37 authReady: boolean; 38 }; 39 40 export const [useAzureAuthSub, AzureAuthSub] = createStore<AzureAuthSubType>({ 41 authHeader: new BrowserHeaders({}), 42 authReady: false, 43 }); 44 45 const getMsalConfig = (configs: GetFrontendConfigResponse): Configuration => ({ 46 auth: { 47 clientId: configs.authConfig?.azureAuth?.clientId || '', 48 authority: `${configs.authConfig?.azureAuth?.cloudInstance || ''}${ 49 configs.authConfig?.azureAuth?.tenantId || '' 50 }`, 51 redirectUri: configs.authConfig?.azureAuth?.redirectUrl || '', 52 }, 53 cache: { 54 cacheLocation: 'sessionStorage', 55 storeAuthStateInCookie: false, 56 }, 57 }); 58 59 // - Email scope was added later so kuberpult can extract the email from requests (the author) 60 // and send it along to the backend 61 const loginRequest = { 62 scopes: ['email'], 63 }; 64 65 // exported just for testing 66 export const Utf8ToBase64 = (str: string): string => window.btoa(unescape(encodeURIComponent(str))); 67 68 export const AcquireToken: React.FC<{ children: React.ReactNode }> = ({ children }) => { 69 const { instance, accounts } = useMsal(); 70 71 const { authReady } = useAzureAuthSub((auth) => auth); 72 73 React.useEffect(() => { 74 const request = { 75 ...loginRequest, 76 account: accounts[0], 77 }; 78 const email: string = Utf8ToBase64(accounts[0]?.username || ''); // yes, the email is in the "username" field 79 const username: string = Utf8ToBase64(accounts[0]?.name || ''); 80 instance 81 .acquireTokenSilent(request) 82 .then((response: AuthenticationResult) => { 83 AzureAuthSub.set({ 84 authHeader: new BrowserHeaders({ 85 Authorization: response.idToken, 86 'author-email': email, // use same key here as in server.go function getRequestAuthorFromAzure: r.Header.Get("email") 87 'author-name': username, // use same key here too 88 }), 89 authReady: true, 90 }); 91 }) 92 .catch((silentError) => { 93 instance 94 .acquireTokenPopup(request) 95 .then((response: AuthenticationResult) => { 96 AzureAuthSub.set({ 97 authHeader: new BrowserHeaders({ 98 Authorization: response.idToken, 99 'author-email': email, // use same key here as in server.go function getRequestAuthorFromAzure: r.Header.Get("email") 100 'author-name': username, // use same key here too 101 }), 102 authReady: true, 103 }); 104 }) 105 .catch((error) => { 106 // eslint-disable-next-line no-console 107 console.error('acquireTokenSilent failed: ', silentError); 108 // eslint-disable-next-line no-console 109 console.error('acquireTokenPopup failed: ', error); 110 }); 111 }); 112 }, [instance, accounts]); 113 114 if (!authReady) { 115 return <div>loading...</div>; 116 } 117 return <>{children}</>; 118 }; 119 120 export const AzureAutoSignIn = (): JSX.Element => { 121 const isAuthenticated = useIsAuthenticated(); 122 const { instance } = useMsal(); 123 React.useEffect(() => { 124 if (!isAuthenticated) { 125 instance.loginRedirect(loginRequest); 126 } 127 }, [instance, isAuthenticated]); 128 return <>Redirecting to login</>; 129 }; 130 131 export const AzureAuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 132 const { configs, configReady } = useFrontendConfig((c) => c); 133 const msalInstance = React.useMemo(() => new PublicClientApplication(getMsalConfig(configs)), [configs]); 134 if (!configReady) { 135 return <Spinner message={'Loading Configuration'} />; 136 } 137 138 const useAzureAuth = configs.authConfig?.azureAuth?.enabled; 139 if (!useAzureAuth) { 140 AzureAuthSub.set({ authReady: true }); 141 return <>{children}</>; 142 } 143 144 return ( 145 <MsalProvider instance={msalInstance}> 146 <AuthenticatedTemplate> 147 <AcquireToken>{children}</AcquireToken> 148 </AuthenticatedTemplate> 149 <UnauthenticatedTemplate> 150 <AzureAutoSignIn /> 151 </UnauthenticatedTemplate> 152 </MsalProvider> 153 ); 154 };