Shifty
Shifty

Reputation: 143

How do I integrate the cognito hosted UI into a react app?

I am creating a react app - using create-react-app and amplify - and I am trying to set up authentication. I don't seem to be able to handle the federated logins using the hosted UI.

There are some pages which require no authentication to reach and then some which require a user to be logged in. I would like to use the hosted UI since that's prebuilt. I have been following the getting started docs here: https://aws-amplify.github.io/docs/js/authentication

For background I have the following components: - Amplify - an amplify client which wraps calls in methods like doSignIn doSignOut etc. The idea is to keep all this code in one place. This is a plain javascript class - Session - provides an authentication context as a React context. This context is set using the amplify client. It has HOC's for using the context - Pages - some wrapped in the session HOC withAuthentication which only renders the page if the user has logged in

This structure is actually taken from a Firebase tutorial: https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial/ Maybe this is just not feasible with Amplify? Though the seem similar enough to me that it should work. The basic idea is that the Session provides a single auth context which can be subscribed to by using the withAuthentication HOC. That way any component that requires a user will be rendered as soon as a user has logged in.

Originally I wrapped the entire App component in the withAuthenticator HOC provided by amplify as described in the docs. However this means that no pages are accessible without being authenticated - home page needs to be accessible without an account.

Next I tried calling to the hosted UI with a sign in button and then handling the response. The problem is when the hosted UI has logged a user in then it redirects back to the app causing it to reload - which is not ideal for a single page app.

Then I tried checking if the user is authenticated every time the app starts - to deal with the redirect - but this becomes messy as I need to move a lot of the amplify client code to the Session context so that it can initialise correctly. The only way I can see to get this is using the Hub module: https://aws-amplify.github.io/docs/js/hub#listening-authentication-events The downside is that after logging in, the app refreshes and there's still a moment when you are logged out which makes the user experience weird.

I would have thought that there would be a way to not cause an application refresh. Maybe that's just not possible with the hosted UI. The confusing thing to me is that the documentation doesn't mention it anywhere. In actual fact there is documentation around handling the callback from the hosted UI which as far as I can see never happens because the entire page refreshes and so the callback can never run.

I've tried to trim this down to just what's needed. I can provide more on request.

Amplify:

import Amplify, { Auth } from 'aws-amplify';
import awsconfig from '../../aws-exports'; 
import { AuthUserContext } from '../Session';

class AmplifyClient {
    constructor() {
        Amplify.configure(awsconfig);
        this.authUserChangeListeners = [];
    }

    authUserChangeHandler(listener) {
        this.authUserChangeListeners.push(listener);
    }

    doSignIn() {
        Auth.federatedSignIn()
            .then(user => {
                this.authUserChangeListeners.forEach(listener => listener(user))
            })
    }

    doSignOut() {
         Auth.signOut()
            .then(() => {
                this.authUserChangeListeners.forEach(listener => listener(null))
            });
    }
}

const withAmplify = Component => props => (
    <AmplifyContext.Consumer>
        {amplifyClient => <Component {...props} amplifyClient={amplifyClient} />}
    </AmplifyContext.Consumer>
);

Session:

const provideAuthentication = Component => {
    class WithAuthentication extends React.Component {
           constructor(props) {
            super(props);

            this.state = {
                authUser: null,
            };
        }

        componentDidMount() {
            this.props.amplifyClient.authUserChangeHandler((user) => {
                this.setState({authUser: user});
            });
        }

        render() {
            return (
                <AuthUserContext.Provider value={this.state.authUser}>
                    <Component {...this.props} />
                </AuthUserContext.Provider>
            );
        }
    }

    return withAmplify(WithAuthentication);
};

const withAuthentication = Component => {
    class WithAuthentication extends React.Component {
        render() {
            return (
                <AuthUserContext.Consumer>
                    {user =>
                        !!user ? <Component {...this.props} /> : <h2>You must log in</h2>
                    }
                </AuthUserContext.Consumer>
            );
        }
    }

    return withAmplify(WithAuthentication);
};

The auth context is provided once at the top level:

export default provideAuthentication(App);

Then pages that require authentication can consume it:

export default withAuthentication(MyPage);

What I would like to happen is that after the user signs in then I can set the AuthUserContext which in turn updates all the listeners. But due to the redirect causing the whole app to refresh the promise from Auth.federatedSignIn() can't resolve. This causes the user to be displayed with You must log in even though they just did.

Is there a way to block this redirect whilst still using the hosted UI? Maybe launch it in another tab or in a popup which doesn't close my app? Or am I going about this the wrong way? It just doesn't feel very 'Reacty' to cause full page refreshes.

Any help will be greatly appreciated. I can provide more details on request.

Upvotes: 7

Views: 5848

Answers (1)

J. Hesters
J. Hesters

Reputation: 14766

Instead of chaining onto the Auth's promise, you can use Amplify's build-in messaging system to listen to events. Here is how I do it in a custom hook and how I handle what gets rendered in Redux.

import { Auth, Hub } from 'aws-amplify';
import { useEffect } from 'react';

function useAuth({ setUser, clearUser, fetchQuestions, stopLoading }) {
  useEffect(() => {
    Hub.listen('auth', ({ payload: { event, data } }) => {
      if (event === 'signIn') {
        setUser(data);
        fetchQuestions();
        stopLoading();
      }
      if (event === 'signOut') {
        clearUser();
        stopLoading();
      }
    });

    checkUser({ fetchQuestions, setUser, stopLoading });
  }, [clearUser, fetchQuestions, setUser, stopLoading]);
}

async function checkUser({ fetchQuestions, setUser, stopLoading }) {
  try {
    const user = await Auth.currentAuthenticatedUser();
    setUser(user);
    fetchQuestions();
  } catch (error) {
    console.log(error);
  } finally {
    stopLoading();
  }
}

Upvotes: 2

Related Questions