amplify-js icon indicating copy to clipboard operation
amplify-js copied to clipboard

Login with popup functionality

Open maccoda opened this issue 2 years ago • 14 comments

Is this related to a new or existing framework?

No response

Is this related to a new or existing API?

Authentication

Is this related to another service?

No response

Describe the feature you'd like to request

We are currently using federated login with a custom provider through the Cognito hosted UI. To support our current use case we are needing to do federated login via a popup as it is being integrated via an iframe. It would be great if this could be supported natively for the web API.

Describe the solution you'd like

A solution similar to that provided by Auth0 would be great where it is part of the SDK. I believe here they exchange the code in the oauth flow through window messaging.

Describe alternatives you've considered

I have looked into implementing a customer urlOpener as part of the configuration but this does not work as it is not able to communicate back to the parent window for the authentication exchange as this is more in the internals of the Amplify API.

Additional context

No response

Is this something that you'd be interested in working on?

  • [ ] 👋 I may be able to implement this feature request
  • [ ] ⚠️ This feature might incur a breaking change

maccoda avatar Oct 25 '23 21:10 maccoda

Hi @maccoda thanks for opening this issue. Once question to confirm your usage - are you doing this in React Native or on a web application?

If in a web application, could you elaborate on your use case to understand the need for using an iframe?

nadetastic avatar Oct 26 '23 17:10 nadetastic

Hey @nadetastic. This is in a web application. We need an iframe as it is the method to provide a custom integration into an existing software platform. We want to be able to surface our application within another one and iframes are how that is being done. We want to also ensure though that only certain users are allowed to access our customer application hence why we have our own layer of authentication. The team that owns the application that we are embedding into have provided an example of the popup workflow which works as intended and I know they are using Auth0 hence why I referenced it.

maccoda avatar Oct 26 '23 22:10 maccoda

@nadetastic is it tracked as Feature Request? Is there any way to implement federatedSignIn with a custom provider using popup window in Angular?

mordka avatar Mar 28 '24 12:03 mordka

Hey, @mordka 👋. We are definitely still tracking this as a feature request, but don't have any updates at this time. With the way Amplify currently works, you'll have to be redirected for federated logins. If you have any additional context or input in what you'd like to see with this feature, feel free to add it and upvote this issue.

cwomack avatar Mar 29 '24 19:03 cwomack

Any updates?

therealtgd avatar May 21 '24 11:05 therealtgd

Just throwing in that this would be a lovely feature. Hitting the same thing and no idea how to get around it. Was hoping there might be a work around in here at least.

PeteDuncanson avatar Feb 19 '25 15:02 PeteDuncanson

@PeteDuncanson, thanks for chiming in here. Are you able to provide any additional details on the problem you are trying to solve here? We're interested in understanding the use cases surrounding this particular feature.

jjarvisp avatar Feb 19 '25 20:02 jjarvisp

We ended up needing to make a workaround and built our own window pop handler that would open a new popup window that would do the redirection for the federated auth and then the original page would be waiting for the closure of the popup to refresh.

maccoda avatar Feb 20 '25 00:02 maccoda

@jjarvisp hello 👋

A little context first then the proposed solution:

We are using a live chat service called re:amaze. In their UI for our team to use to answer chats they allow an iframe hosted widget that can call out to other services/app that we control. We've built a page on our system that tries to look up customer information from the email/phone number provided by the customer so our operators have some details to go on during their chat. A nice addition to the workflow.

However what worked while developing locally (vai ngrok) doesn't work in production. The widget always asks us to login even if we've already been logged in in another tab. This part really confuses me as I would have thought the auth cookies for the domain would still be passed around but seemingly not (anyone got any ideas?). When we try to login it jumps off to Google but at this point fails as google sends a header to prevent it being displayed in an iframe.

Image

So we are a bit stuck and I can't see any way around it or find any docs hence me ending up here 😄

The solution that Google suggest is to open a new pop up window to host the google login part which will then call back within the window to a new page that can then send a message to the iframe page to allow us through. Sounds similar to what @maccoda has described.

Trouble is I'm not as smart as @maccoda as I'm unsure how to get my iframe code to know that we are now logged in. Sending a message to it and handling the redirect I could do...but after that...what? How to get it to know its authorised?

Would be fantastic if this was built in but I know thats going to take a while to get released if it ever makes it onto the roadmap in the first place so as a stop gap if anyone can describe a better work around or how to tell my app its authorised from a pop up window I'm all ears.

PeteDuncanson avatar Feb 20 '25 10:02 PeteDuncanson

A little more on this as I've been playing around with it on and off all day.

✅ I've got a pop up that launched if my app is within an iframe when I click the sign in button. ✅ Google either asks me to sign in or reloads my page and lets me through within the popup (depends on if I'm already signed in via another tab or not) ✅ My app then loads up within the popup and fires out a message event but no payload as yet as I'm not sure what to send it ✅ My iframe hosted app receives the message

Now what I'm missing is: 🛑 What to pass via the message? 🛑 What should my iframed app do to authorise my app?

This is roughly what I have fleshed out within my main React component that is doing the listening, this has access to AWS Amplifies Auth stuff:

  useEffect(() => {
    const handleAuthMessage = (event) => {
      if (event.origin !== "https://my-popup-domain.com") {
        return; // Security: Ensure only messages from your domain are accepted
      }

      Auth.whatToDoHere????()
          .then((user) => console.log("User authenticated:", user))
          .catch(() => {
            // If user session isn't recognized
          });

        // Optionally reload the page to refresh the authentication state
        window.location.reload();
      }
    };

    window.addEventListener("message", handleAuthMessage);

    return () => window.removeEventListener("message", handleAuthMessage);
  }, []);

I've no idea what to do at this point!

I hope that is enough context to point someone in the right direction to help come up with a "try this" solution to get this unstuck. In the meantime I'm on to another work around as a backup via a standalone API and a static web page we can hit to do the work this is trying to do. I'd rather have it all within my app though and keep my surface area to a minimum.

Cheers

Pete

PeteDuncanson avatar Feb 20 '25 16:02 PeteDuncanson

Hey @PeteDuncanson, thanks for the detailed description of your use case! This is very helpful.

Due to some security minded browser behavior, it's not possible to utilize cookies from within the context of a third party (iframe). This means cookies from within the third party context won't be accessible by the browser even if they are available from a previous login in another tab.

We will explore whether this behavior is something we can accomplish through alternative methods but we don't necessarily have any recommended workarounds at this point. We will follow up here with any additional information.

jjarvisp avatar Feb 20 '25 21:02 jjarvisp

Hi all,

For future me and anyone else who might be following (stumbling) along with this need I've got a work around that seems to do what I want. Thought I'd share. If anyone sees any issues with this let me know and happy to tweak it.

For context I'm using Google as my social login source and I'm developing with React. When we start the login process we store some ids locally, link off to Google with those ids and a redirect callback. Google does its thing and then should pass back to us 2 query string params called "code" and "state" (thank you Network tab and persisted logs!)

First up I changed my login page with two changes:

  1. The sign in button now can launch a popup if we are within an iframe and if we aren't then it just does links to google in window as normal:
  <button
    onClick={() => {
      if (window.self !== window.top) {
        // If in an iframe, open the same page in a new window
        window.open(
          window.location.href,
          "_blank",
          "width=500,height=600"
        );
      } else {
        // If not in an iframe, proceed with the normal login
        googleLogin(window.location.pathname);
      }
    }}
  >
    Sign in
  </button>

Within the same page I have a useEffect that will fire on page load that will kick start the google login automatically if we are within a popup (there is quite a bit of this "Am I in a popup or an iframe" logic, sorry, just the way it is).

  useEffect(() => {
    if (window.opener && window.location.search.indexOf("code") === -1) {
      // If a popup window then try logging in automatically
      googleLogin(window.location.pathname);
    }
  });

For completeness my googleLogin function looks like this and doesn't need to differ from normal to allow for this popup stuff to work:

  const googleLogin = useCallback(
    (redirectPath: string) => {
      Auth.federatedSignIn({ provider: CognitoHostedUIIdentityProvider.Google })
        .then((cognitoUser) => {
          setLoginRedirectPath(redirectPath);
          setCognitoUser(cognitoUser);
          return cognitoUser;
        })
        .catch((err) => {
          if (err.code === "UserNotFoundException") {
            err.message = "Invalid username or password";
          }
          throw err;
        })
        .finally(() => {
          setIsCognitoLoading(false);
        });
    },
    [setCognitoUser, setLoginRedirectPath, setIsCognitoLoading]
  );

I now needed some scripts to run instantly whenever my hosting page (the static index.html page that my React app sits in) is loaded. This is needed so it catches any callbacks from Google and redirect the values from it to the iframed page before the rest of my code runs. So I added this to the index.html of my app and this page is loaded first by both the popup and my iframe so this code handles both situations to enable us to communicate between them and pass the login details:

    <script>
      // If we are within an iframe we have to do some popup magic to get us to login.
      // This code handles the callback from google and reloads the page with the passed in
      // auth codes that google sent to the popup window. In theory this should then
      // allow this iframed version of our app to login 
      (function () {
        // Not required but helped me get my head around the differing storage between popup and iframe. The iframe is 
       // content considered to be within a 3rd party page so your access to read local storage etc is limited hence it not 
       // working in the first place. Check your DevTools > Application > Session storage to see that this is set independently 
       // in the popup and the iframed page. Setting it in either one doesn't mean the other gets it, which is why we have to 
       // do the below  workaround
        sessionStorage.setItem("session_test", "logged_in");

        // WITHIN POPUP WINDOW: If we are the popup window and we have the auth token response 
        // then we need to forward it on to the iframe code
        function getRelevantLocalStorage() {
          const storedData = {};
          for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (
              key.includes("Google_") ||
              key.includes("CognitoIdentityServiceProvider")
            ) {
              storedData[key] = localStorage.getItem(key);
            }
          }
          return storedData;
        }

        function getSessionStorage() {
          const storedData = {};
          for (let i = 0; i < sessionStorage.length; i++) {
            const key = sessionStorage.key(i);
            storedData[key] = sessionStorage.getItem(key);
          }

          return storedData;
        }

        if (window.location.search && window.opener) {
          console.info("Login code running now from popup");
          // Get the values posted back from Google on the querystring
          const params = new URLSearchParams(window.location.search);
          const code = params.get("code");
          const state = params.get("state");

          if (code && state) {
            // Construct the message to send back to the opener
            const message = {
              type: "google-auth-success",  // This could be called anything, its custom to this code
              urlParams: {
                code: code,
                state: state,
              },
              localStorageData: getRelevantLocalStorage(),  // Needed to record IF we are logged in for future reloads/visits
              sessionStorageData: getSessionStorage(),  // Needed to actually login this first time
            };

            // Send the message to the opener window (ie the iFramed app)
            if (window.opener) {
              console.info("Opener sending message", message);
              window.opener.postMessage(message, window.location.origin);
            } else {
              console.error("No opener window found");
            }

            // Optionally close the popup window for a nice UI feel
            window.close();
          } else {
            console.error("Missing code or state in query parameters");
          }
        }

        // WITHIN IFRAMED WINDOW: Action any message we received from our popup window code (see above)
        if (window.self !== window.top) {
          console.info("Login code running now from iframe");
          window.addEventListener("message", (event) => {
            console.info(
              "Iframed page, hey I got a message!",
              event.data.localStorageData
            );

            // Security check of sorts, we only want to handle messages from our own stuff
            if (event.origin !== window.location.origin) return;

            if (event.data && event.data.type === "google-auth-success") { 
              const { code, state } = event.data.urlParams;

              console.log("Received code:", code);
              console.log("Received state:", state);

              // Store Google & Cognito localStorage values in the iframe
              for (const [key, value] of Object.entries(
                event.data.localStorageData
              )) {
                localStorage.setItem(key, value);
              }

              // Store Google & Cognito sessionStorage values in the iframe
              for (const [key, value] of Object.entries(
                event.data.sessionStorageData
              )) {
                sessionStorage.setItem(key, value);
              }

              // Rebuild the google callback url and reload our iframed content to it which should complete the login!
              window.location.href = `${window.location.origin}/?code=${code}&state=${state}`;
            }
          });
        }
      })();
    </script>

I've tried this locally and on my staging enviroment and it works really well. Once logged in via this method the iframe remembers you are logged in (thanks to passing and setting the localstorage) so future loads of the iframed app should remember your logged in state which is all I wanted. I'm sure I'll tighten up the UX a little more but for now this has solved a right PITA for me and I did a little mini fist bump to myself when I got it working.

Hope it helps some other code traveller out there. Open to any feedback as ever.

Pete

PeteDuncanson avatar Feb 24 '25 21:02 PeteDuncanson

Hey @PeteDuncanson, thanks for sharing your workaround with the community! Glad to hear you've found the solution. One feedback from us is that our general recommendation is to set signIn redirect URL using Amplify.configure.

Regarding this feature request, we will be keeping this open. Please let us know if you have further question, or suggestion!

joon-won avatar Feb 26 '25 18:02 joon-won

Any timeline with this, would be great to have the functionality if not at least something that doesn't automatically redirect you so that we can handle the popup ourselves and give it back to the library.

Bituhh avatar May 29 '25 07:05 Bituhh