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

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': () => {
...
}

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),
})

Updating Vue state during navigation with vue-router

I'm building a Vue 3 app which uses Vue Router and Pinia for state management.
In my global state I'm defining a property which tells me if a user is logged in so the user can navigate through the app. However, the login process is being handled by Cognito, so once I enter the app, I click on the login button, it takes me to a Cognito screen which handles the login process and then redirects me back to my app's home page.
So after the redirect I extract some params from the resulting URL and I want to save them in the global state, for this I was using the beforeEach guard to parse the URL, check for the params, update the store and then reading from it to check the valid session.
But my issue was that the navigation continued even before the state was updated. I ended up using setTimeout just to see if waiting for a bit solved the issue and it did
router.beforeEach((to) => {
const mainStore = useMainStore();
if (to.name === 'Login') {
return;
}
if (to.hash !== '') {
const queryParams = window.location.href.split('&');
const paramValues = queryParams.map(param => {
})
const payload = {
id_token: paramValues.find(param => param.name === 'id_token').value,
access_token: paramValues.find(param => param.name === 'access_token').value,
token_type: paramValues.find(param => param.name === 'token_type').value,
isAuthenticated: true
}
//Here I'm trying to update my global state
mainStore.updateTokens(payload);
}
// Here I use timeout, but before I just had the check for the property
setTimeout(() => {
if (!mainStore.isAuthenticated) return '/login';
}, 3000)
});
How should I handled this case? I've read about the beforeResolve guard but I'm not sure on how to use it; basically I just need to know how should I go about performing some async operation (like fetching data from server) during the navigation, not inside components.

Is there a way to make protected route in sapui5

getRouter: function () {
return UIComponent.getRouterFor(this);
},
onInit: function () {
firebase.auth().onAuthStateChanged((user) => {
if (!user) {
this.getRouter().navTo("login");
}
});
}
Is there a way to make my routes protected, it is no problem when the first time I type in browser, I got redirected, but if I logout from the component, and manually type in browser /admin, I am not redirected, so onInit works only one time. I am new to sapui5, and in React it is different. Can someone please help and explain if there is a way to make some kind of protection on routes. Thanks.
The correct event to listen for is patternMatched
This is an example of how to protect a single route called "myProtectedRoute":
onInit: function () {
this.getRouter().getRoute("myProtectedRoute").attachPatternMatched((event) => {
if (user.isAuthenticated) {
// load and show data
} else {
this.getRouter().navTo("login");
}
})
}
If you wish to be notified when any route is matched you can read more in this tutorial https://openui5.hana.ondemand.com/topic/4a063b8250f24d0cbf7c689821df7199

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

How to make middleware to finish asynchronous request to the server and commit to Vuex store before the page loads?

My middleware uses firebase.auth().onAuthStateChanged and stores the user object in the Vuex store in case if it exists. The code works fine, but the page loads before the user object in Vuex store gets set. My another middleware protects certain pages by checking if the user object exists. Thus, if I load http://localhost:3000/protected, I would be declined access, but if I first load another page and proceed to http://localhost:3000/protected through a <nuxt-link> I can access the page.
How do I make nuxt wait for the middleware to setUser before loading pages?
export default async function (context, callback) {
auth.onAuthStateChanged(user => {
if (user) {
context.store.commit("setUser", user)
callback()
} else {
callback()
}
})
}
Update:
Plugins approach yields the same result
Here you can read an example of authentication in Vue: https://scotch.io/tutorials/vue-authentication-and-route-handling-using-vue-router
Basically you can check authentication before letting user open the protected component. The easiest way to achieve this is using router guards. In your router definitions:
{
path: '/proctected',
beforeEnter(to, from, next) {
if (isAuthenticated()) {
next();
} else {
next('/page-to-show-for-unauthenticated-users');
}
}
}
This guard will protect from entering /proctected url. Here you can check the working codepen: https://codepen.io/anon/pen/JwxoMe
More about router guards: https://router.vuejs.org/guide/advanced/navigation-guards.html#per-route-guard

Categories

Resources