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  };