Skip to content
Snippets Groups Projects
OAuthProvider.tsx 5.88 KiB
Newer Older
import { useCallback, useRef, useState } from "react"
import { OAuthContext } from "./OAuthContext"
import { IOAuthState, initialOAuthState } from "./OAuthState"
import { OAUTH_RESPONSE_MESSAGE_TYPE, OAUTH_STATE_STORAGE_KEY } from "../../commons/costants"
import { tokenPostRequest } from "./OAuthTokenRequest"
import { OidcToken, OidcClientSettings } from "./OidcConfig"
import { User } from "./User"


interface OAuthProviderProps extends OidcClientSettings {
  children?: React.ReactNode;
}

type Timer = ReturnType<typeof setTimeout>;


const getAuthorizationUrl = (props: OidcClientSettings) => {
  let url = `${props.authority}` +
    `/authorize?` +
    `&redirect_uri=${props.redirect_uri}` +
    `&client_id=${props.client_id}`

  url += props.client_secret ? `&client_secret=${props.client_secret}` : "";
  url += props.response_type ? `&response_type=${props.response_type}` : "";
  url += props.scope ? `&scope=${props.scope}` : "";
  url += props.state ? `&state=${props.state}` : "";
  return url;
}

const generateState = () => {
  const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let array = new Uint8Array(40);
  window.crypto.getRandomValues(array);
  array = array.map((x) => validChars.codePointAt(x % validChars.length) || 0);
  const randomState = String.fromCharCode.apply(null, Array.from(array));
  return randomState;
};

const saveState = (state: string) => {
  sessionStorage.setItem(OAUTH_STATE_STORAGE_KEY, state);
}

const openWindow = (url: string) => {
  return window.open(url);
}

const closePopup = (popupRef: React.MutableRefObject<Window | undefined>) => {
  popupRef.current?.close();
}

const cleanup = (
  intervalRef: React.MutableRefObject<Timer | undefined>,
  popupRef: React.MutableRefObject<Window | undefined>,
  handleMessageListener: (message: any) => Promise<void>) => {
  clearInterval(intervalRef.current);
  closePopup(popupRef);
  window.removeEventListener('message', handleMessageListener);
}

const parseJwt = (token: string) => {
  var base64Payload = token.split('.')[1];
  var payload = window.atob(base64Payload);
  return JSON.parse(payload);
}

export const OAuthProvider = (props: OAuthProviderProps): JSX.Element => {
  const { children } = props;
  const [oAuthState, setOAuthState] = useState<IOAuthState>(initialOAuthState);
  const popupRef = useRef<Window>();
  const intervalRef = useRef<Timer>();

  const signinPopup = useCallback(() => {
    // 1. Init 
    setOAuthState(initialOAuthState);

    // 2. Generate and save state
    const state = generateState();
    saveState(state);

    // 3. Open window
    const url = getAuthorizationUrl({ ...props, state });
    popupRef.current = openWindow(url) || undefined;

    // 4. Register message listener
    async function handleMessageListener(message: any) {

      if (message?.data?.type !== OAUTH_RESPONSE_MESSAGE_TYPE) {
        return;
      }

      try {
        const errorMaybe = message && message.data && message.data.error;
        if (errorMaybe) {
          setOAuthState({
            isLoading: false,
            isAuthenticated: false,
            error: errorMaybe && new Error(errorMaybe)
          });
          console.log(errorMaybe);
        } else {
          const { code } = message && message.data && message.data.payload;
          // Sent POST request to backend
          tokenPostRequest({
            client_id: props.client_id,
            redirect_uri: props.redirect_uri,
            code: code,
            grant_type: props.grant_type,
            code_verifier: undefined
          })
            .then(response => response.json())
            .then(data => {
              const token = OidcToken.createTokenFromResponse(data);
              const user = new User({
                session_state: null,
                profile: parseJwt(token.access_token),
                token: token
              });
              // The user is now logged in
              console.log(parseJwt(token.access_token));
              setOAuthState({
                isLoading: false,
                isAuthenticated: true,
                user: user
              });
            })
            .catch((err) => {
              console.error(err);
              setOAuthState({
                isLoading: false,
                isAuthenticated: false,
                error: err instanceof Error ? err : new Error("Uknown error")
              });
            });
        }
      } catch (err) {
        setOAuthState({
          isLoading: false,
          isAuthenticated: false,
          error: err instanceof Error ? err : new Error("Uknown error")
        });
        console.log(err);
      } finally {
        cleanup(intervalRef, popupRef, handleMessageListener);
      }
    }
    window.addEventListener('message', handleMessageListener);

    // 5. Begin interval to check if popup was closed forcelly by the user
    intervalRef.current = setInterval(() => {
      const popupClosed = !popupRef.current
        || !popupRef.current.window
        || popupRef.current.window.closed;
      if (popupClosed) {
        setOAuthState({
          isAuthenticated: false,
          isLoading: false,
        });
        console.warn("Warning: Popup was closed before completing authentication.");
        cleanup(intervalRef, popupRef, handleMessageListener);
      }
    }, 250);

    // 6. Remove listener(s) on unmount
    return () => {
      window.removeEventListener('message', handleMessageListener);
      if (intervalRef.current) clearInterval(intervalRef.current);
    };

  }, [props]);

  const logout = useCallback(() => {
    setOAuthState(initialOAuthState);
  }, []);

  return (
    <OAuthContext.Provider
      value={{
        signinPopup: signinPopup,
        logout: logout,
        isAuthenticated: oAuthState.isAuthenticated,
        isLoading: oAuthState.isLoading,
        error: oAuthState.error,
        user: oAuthState.user
      }}
    >
      {children}
    </OAuthContext.Provider>
  )
}