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

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.

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

Related

login an anonymous user forever in Firebase with React Native

I'm doing a scheduler (calendar) app using React Native. You can add tasks to whatever day you want to do them. To store those tasks I'm using Firebase.
Now, I want that the tasks shown in each device are different. I though the best way to achieve this is logging the user anonymously (so that user doesn't have to sign up, which would be nonsense, in my opinion), and by doing so, only tasks owned by that user can show.
I have achieved signing up a new anonymous user (and I can see the new user in the project in Firebase web page). However, I'm struggling to make that logging last forever.
I had written this piece of code in the first function that my app renders:
useEffect(() => {
auth.onAuthStateChanged(function (user) {
if (user) {
navigation.navigate("Home");
} else {
auth.signInAnonymously().then(navigation.navigate("Home"));
}
});
});
where auth = firebase.auth(). However, each time that I reload my app, a new user is created.
Can anyone help me? Thanks in advance! :)
Have you tried to set RN persistence ?
import {
initializeAuth,
getReactNativePersistence,
} from 'firebase/auth/react-native'
import AsyncStorage from '#react-native-async-storage/async-storage'
export const auth = initializeAuth(app, {
persistence: getReactNativePersistence(AsyncStorage),
})

How to properly configure Firebase Auth in a single page application (SPA)?

I'm having difficulties understanding how to best implement Firebase Auth in a SPA web application. I'm new to both SPAs and Firebase.
My app consists of both secure pages and non-secure pages. The non-secure pages are for things like terms & conditions, privacy policy and forgot password.
Inside my app code, at the highest level e.g. /app.js, I'm importing a Firebase Auth configuration module as the first order of operation. This module contains the following function which listens for changes in authentication and acts accordingly.
firebase.auth().onAuthStateChanged(user => {
if (!user) {
Store.router.navigate("/login"); // <-- this is my problem
} else {
// get user data from Cloud Firestore
// store user data locally
}
});
This is my router at it's basic level:
router.on({
'/': () => {
// import module
},
'/login': () => {
// import module
},
'/forgot-password': () => {
// import module
}
}).resolve();
Before I decided to use Firebase Auth, my router checked for authentication at each route and looked a little like this:
router.on({
'/': () => {
if (isAuthenticated) {
// import module
} else {
router.navigate("/login")
}
},
'/login': () => {
if (!isAuthenticated) {
// import module
} else {
router.navigate("/")
}
},
'/forgot-password': () => {
// import module
}
}).resolve();
Every time a route changes using the Firebase Auth version of my app, the onAuthStateChanged listener receives an update and, if the user is logged out, it redirects them to the /login page. If logged in, it grabs the user's full profile from the database and stores it locally.
Now, this works brilliantly unless the user is logged out, is on the /login page, and wants to visit the /forgot-password page. When a user navigates to this page, or any other no-secure, public page, the authentication listener updates itself and redirects the user back to /login instantly and this is wrong.
This is highly undesirable but I really like the way this listener works other than that, as if/when a user has multiple tabs open and logs out of one, it returns all tabs back to /login.
How can I configure this listener, or reconfigure my app, to allow the public pages to be available too? And should I be unsubscribing from the listener?
I managed to solve the problem, so I'll share my findings here for others. I did however lose the functionality that returned all open tabs to the login page when they logged out of one but this does work better for my app that has public routes.
I now have a method in my User module called getCurrentUser() which is now where the onAuthStateChanged observable sits. Because I used the 'unsubscribe()` method, I can now call this as and when I need it without having it observing continuously.
getCurrentUser: () => {
return new Promise((resolve, reject) => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
unsubscribe();
resolve(user);
}, reject);
})
}
In my router, I can now check the auth state by calling and waiting for User.getCurrentMethod().
router.on({
'/': async () => {
if (await User.getCurrentUser()) {
// import module
// load HTML
} else {
router.navigate('/login')
}
},
'/login': () => {
...
}

AWS Amplify React UI Component - How to dispatch auth state change event?

My application uses AWS Amplify React UI Components (#aws-amplify/ui-react) to handle user flows and I needs to know when a user successfully signs in.
I have added the handleAuthStateChange prop below. This works and I can receive the new state, however it prevents the app from navigating to other AmplifyAuthenticator slots like sign-up and forgotpassword.
<AmplifySignIn
slot="sign-in"
handleAuthStateChange={(state, data) => {
// handle state === 'signedin' but pass along other states
}}
></AmplifySignIn>
Does anyone know how to get notified about changes in authentication state without breaking other AmplifyAuthenticator slots?
You can add it to the AmplifyAuthenticator component.
<AmplifyAuthenticator
handleAuthStateChange={(state, data) => {
console.log(state)
console.log(data)
//add your logic
}}
>
<AmplifySignIn
slot="sign-in"
>
</AmplifySignIn>
</AmplifyAuthenticator>
Or you can access Auth state changes in other components using
import { onAuthUIStateChange } from '#aws-amplify/ui-components'
useEffect(() => {
return onAuthUIStateChange((state, data) => {
console.log(state);
console.log(data);
//add your logic
});
}, []);
Hope this helps

React - correct way to wait for page load?

In React JS, what is the correct way to wait for a page to load before firing any code?
Scenario:
A login service authenticates a user then redirects them (with a cookie), to a React App.
The React App then straight away searches for a cookie and validates it against an endpoint.
But the problem I am getting is when user is authenticated at login service, then forwarded to React App, the App is loading so quick before cookie is even loaded.
What is the right way to fire the code? I tried wrapping in a componentDidMount(), didnt work!
I would suggest you to use a state in the Main component of your application (usually App.jsx) which will control loading. When you start the app the state will be true and only after checking all you need to check it will beacome false. If state is loading you will show a spinner or whatever you want and when it is not loading, the website:
class App extends Component {
constructor(props) {
super(props);
this.state = { loading: true }
}
componentDidMount () {
checkToken()
.then(() => this.setState({ loading: false });
}
if (loading) {
return <Spinner /> // or whatever you want to show if the app is loading
}
return (
...rest of you app
)
}
If any doubt just let me know.
you can use Suspense and lazy :)
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}

Next/React-Apollo: React props not hooked up to apollo cache when query comes from getInitialProps

I'm using nextjs and react-apollo (with hooks). I am trying to update the user object in the apollo cache after a mutation (I don't want to refetch). What is happening is that the user seems to be getting updated in the cache just fine but the user object that the component uses is not getting updated. Here is the relevant code:
The page:
// pages/index.js
...
const Page = ({ user }) => {
return <MyPage user={user} />;
};
Page.getInitialProps = async (context) => {
const { apolloClient } = context;
const user = await apolloClient.query({ query: GetUser }).then(({ data: { user } }) => user);
return { user };
};
export default Page;
And the component:
// components/MyPage.jsx
...
export default ({ user }) => {
const [toggleActive] = useMutation(ToggleActive, {
variables: { id: user.id },
update: proxy => {
const currentData = proxy.readQuery({ query: GetUser });
if (!currentData || !currentData.user) {
return;
}
console.log('user active in update:', currentData.user.isActive);
proxy.writeQuery({
query: GetUser,
data: {
...currentData,
user: {
...currentData.user,
isActive: !currentData.user.isActive
}
}
});
}
});
console.log('user active status:', user.isActive);
return <button onClick={toggleActive}>Toggle active</button>;
};
When I continuously press the button, the console log in the update function shows the user active status as flipping back and forth, so it seems that the apollo cache is getting updated properly. However, the console log in the component always shows the same status value.
I don't see this problem happening with any other apollo cache updates that I'm doing where the data object that the component uses is acquired in the component using the useQuery hook (i.e. not from a query in getInitialProps).
Note that my ssr setup for apollo is very similar to the official nextjs example: https://github.com/zeit/next.js/tree/canary/examples/with-apollo
The issue is that you're calling the client's query method. This method simply makes a request to the server and returns a Promise that resolves to the response. So getInitialProps is called before the page is rendered, query is called, the Promise resolves and you pass the resulting user object down to your page component as a prop. An update to your cache will not trigger getInitialProps to be ran again (although I believe navigating away and navigating back should), so the user prop will never change after the initial render.
If you want to subscribe to changes in your cache, instead of using the query method and getInitialProps, you should use the useQuery hook. You could also use the Query component or the graphql HOC to the same effect, although both of these are now deprecated in favor of the new hooks API.
export default () => {
const { data: { user } = {} } = useQuery(GetUser)
const [toggleActive] = useMutation(ToggleActive, { ... })
...
})
The getDataFromTree method (combined with setting the initial cache state) used in the boilerplate code ensures that any queries fetched for your page with the useQuery hook are ran before the page render, added to your cache and used for the actual server-side rendering.
useQuery utilizes the client's watchQuery method to create an observable which updates on changes to the cache. As a result, after the component is initially rendered server-side, any changes to the cache on the client-side will trigger a rerender of the component.

Categories

Resources