I'm trying to use the emulator only when it is available. connectAuthEmulator does not fail if the emulator is not available (i.e.: if I haven't ran firebase emulators:start). It fails later when I try to make a request.
To do this, I'm fetching the emulator URL (http://localhost:9099) using fetch. If the request succeeds, then I call connectAuthEmulator. Otherwise, I do nothing (uses the configured cloud service).
I have a problem where the fetch works, but connectAuthEmulator throws an error: auth/emulator-config-failed. For some weird reason, this seem to happen only when I'm logged in and I make no requests for about 15 seconds. If I spam requests however, the error never occurs.
import { initializeApp } from "firebase/app";
import { getAuth, connectAuthEmulator } from "firebase/auth";
const app = initializeApp({ /** ... */ });
const auth = getAuth(app);
if (NODE_ENV === "development") {
(async () => {
try {
const authEmulatorUrl = "http://localhost:9099";
await fetch(authEmulatorUrl);
connectAuthEmulator(auth, authEmulatorUrl, {
disableWarnings: true,
});
console.info("🎮 Firebase Auth: emulated");
} catch (e) {
console.info("🔥 Firebase Auth: not emulated");
}
})()
}
export { auth };
Any idea why this happens and how to fix it?
Solution #1
First, try http://127.0.0.1:9099 instead of http://localhost:9099 (don't forget to set this as your emulator host in firebase.json).
Solution #2
On top of using solution #1, try rendering your app after everything Firebase-related has init. You can do this by creating listeners on the status of your emulator connections.
src/config/firebase.ts
import { initializeApp } from "firebase/app";
import { getAuth, connectAuthEmulator } from "firebase/auth";
const app = initializeApp({ /** ... */ });
const auth = getAuth(app);
if (NODE_ENV === "development") {
// Create listener logic
window.emulatorsEvaluated = false;
window.emulatorsEvaluatedListeners = [];
window.onEmulatorsEvaluated = (listener: () => void) => {
if (window.emulatorsEvaluated) {
listener();
} else {
window.emulatorsEvaluatedListeners.push(listener);
}
};
(async () => {
try {
// Use 127.0.0.1 instead of localhost
const authEmulatorUrl = "http://127.0.0.1:9099";
await fetch(authEmulatorUrl);
connectAuthEmulator(auth, authEmulatorUrl, {
disableWarnings: true,
});
console.info("🎮 Firebase Auth: emulated");
} catch (e) {
console.info("🔥 Firebase Auth: not emulated");
}
// Indicate that the emulators have been evaluated
window.emulatorsEvaluated = true;
window.emulatorsEvaluatedListeners.forEach(
(listener: () => void) => {
listener();
}
);
})()
}
export { auth };
src/index.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import "./config/firebase";
const root = createRoot(document.getElementById("root")!);
const Root = () => ({
/** Some JSX */
});
if (NODE_ENV === "development") {
window.onEmulatorsEvaluated(() => {
root.render(<Root />);
});
} else {
root.render(<Root />);
}
Solution #3
Forcibly change the value of auth._canInitEmulator ((auth as unknown as any)._canInitEmulator for TS) to true. This can have some unexpected side-effects (see answer), because some requests might go to your cloud before the emulator kicks in. This can be mitigated with solution #2 (which should prevent requests from firing)
(auth as unknown as any)._canInitEmulator = true;
connectAuthEmulator(auth, authEmulatorUrl, {
disableWarnings: true,
});
Full Answer
Part of the issue here is that I was performing connectAuthEmulator in an async function, when the documentation clearly states it should be called immediately (and synchronously) after getAuth. I was aware of this, but I did not see any other alternative to the issue of connecting to the emulator only when it is available.
I dug into the source code for connectAuthEmulator (permalink) and found out that the error is thrown here:
_assert(
authInternal._canInitEmulator,
authInternal,
AuthErrorCode.EMULATOR_CONFIG_FAILED
);
authInternal._canInitEmulator was false when the error occurred. There is only one place where this property is set to false, in _performFetchWithErrorHandling, here (permalink). This is because it's the first time it performs an API request for the Auth service. So I concluded that Firebase disallows the use of emulators after the first request using auth is made. This is probably to prevent making some requests on your cloud, then switching over to an emulator. So my call to connectAuthEmulator was probably done after a request was already made. I couldn't figure out where though. Even with my app not being loaded (solution #2), the error would still occur.
My conclusion was right, as I later found this error.ts file which pretty much says this:
[AuthErrorCode.EMULATOR_CONFIG_FAILED]:
'Auth instance has already been used to make a network call. Auth can ' +
'no longer be configured to use the emulator. Try calling ' +
'"connectAuthEmulator()" sooner.',
This whole investigation could've been done earlier if FirebaseError had shown this message instead of just auth/emulator-config-failed. It is visible if you console.log(error.message), but I was not aware of that at the time.
For some reason, using the localhost IP instead fixed it instantly and I never had the error again. I added the solution #2 and #3 as another measure after.
try http://127.0.0.1:9099 instead of http://localhost:9099 (don't forget to set this as your emulator host in firebase.json).
I think the root cause of this problem comes from React.StrictMode. This mode will re-render twice time. It might have been initiated twice time too. I am pretty sure because when I disabled the React.StrictMode and refreshed the browser many times. The error does not show up anymore.
Related
I am trying to implement route protection using the new Next.js 12 middleware function. But it occurs to me that every time I try to access the session I get null. Hence not getting the expected result. Why is that?
import { NextResponse, NextRequest } from 'next/server'
import { getSession } from "next-auth/react"
//This acts like a middleware, will run every time for pages under /dashboard, also runs for the nested pages
const redirectUnauthenticatedUserMiddleware = async (req, ev) => {
const session = await getSession({ req })
if (!session) {
return NextResponse.redirect('/login')
}
return NextResponse.next()
}
export default redirectUnauthenticatedUserMiddleware
There's another ways on how you can get access to your session.
Here we're using getToken from next-auth/jwt.
to note:
must use or declared secret as an option in NextAuth function (related reference here). If not, it will not work. Doesn't matter if you use session or jwt option. Both will work.
export default NextAuth({
secret: process.env.SECRET,
...otherOptions
})
currently I don't know whether it will work or not if you don't use token. Or you use database option only. Or adapter. I'm not sure since currently I'm unable to make adapter work just yet. So that's that.
Here is an example _middleware.js:-
import { getToken } from 'next-auth/jwt';
import { NextResponse } from 'next/server';
export async function middleware(req, ev) {
// 'secret' should be the same 'process.env.SECRET' use in NextAuth function
const session = await getToken({ req: req, secret: process.env.SECRET }); console.log('session in middleware: ', session)
if(!session) return NextResponse.redirect('/')
return NextResponse.next()
}
If you already found a solution on how to make use getSession. You may upload your answer and share with us. Thank you.
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': () => {
...
}
I have a cart page written with VueJs and Vuex. I have an api file that acts as a wrapper around axios.
In my vuex action, I call the API and if it was successful I commit data into my mutation.
async await mounted () {
const result = this.getStatus()
if (result === "locked") {
this.$router.push({name: "LockedPage"}
}
else if (result === "expired") {
this.$router.push({name: "SessionExpiredPage"}
}
doSomething(result)
},
methods: {
function doSomething(res) {
// does something with result
}
}
The getStatus function here is from my vuex action.
const {authid, authpwd} = router.history.current.params
if (!authid || !authpwd) {
router.push({name: "SomethingWrong"})
return
}
const res = await api.getStatus
commit("SET_STATUS", res.status)
if (res.otherLogic) {
//commit or trigger other actions
}
return status
}
What's the ideal way to handle these kind of API errors? If you look you'll see that I'm routing inside the Outer component's mounted hook as well as inside the vuex action itself. Should all the routing for this status function just happen inside the vuex action?
I think how it is currently set up, when theSomethingWrong page get's routed. It'll return execution back to the mounted function still. So technically the doSomething function can still be called but I guess with undefined values. This seems kind of bad. Is there a way to stop code execution after we route the user to the error page? Would it be better to throw an error after routing the page?
Should I use Vue.config.errorHandler = function (err, vm, info) to catch these custom route erorrs I throw from the vuex action?
For general errors like expired session I would recommend handle it low at axios level, using interceptor. https://github.com/axios/axios#interceptors
Interceptors can be defined eg. in plugins. Adding Nuxt plugin as example (Vue without Nuxt will use little bit different plugin definition, but still it should be useful as inspiration) (Also window is access because snippet is from SPA application - no server side rendering)
export default ({ $axios, redirect }) => {
$axios.onError((error) => {
const code = parseInt(error.response && error.response.status)
if (code === 401) {
// don't use route path, because error is happending before transition is completed
if (!window.localStorage.getItem('auth.redirect')) {
window.localStorage.setItem('auth.redirect', window.location.pathname)
}
redirect('/login-page')
}
})
}
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();
}
}
I'm developing a React Chrome Extension using the React-Chrome-Redux library
It's the first time I develop using this and I stuck in an error that I can't figure out the reason.
My popup app is failing on runtime with the following error message on the console:
Error in event handler for (unknown): TypeError: Cannot read property
'error' of undefined
I tried to debug and set a breakpoint in the exact location of the error:
return new Promise(function (resolve, reject) {
chrome.runtime.sendMessage({
type: _constants.DISPATCH_TYPE,
payload: data
}, function (_ref2) {
var error = _ref2.error;
var value = _ref2.value;
if (error) {
reject((0, _assignIn2.default)(new Error(), error));
} else {
resolve(value.payload);
}
});
});
}
on the Promise callback the _ref2 is undefined when the action is:
type: "chromex.dispatch" and the payload is also undefined.
This started happening after introduce a dispatch method to start the authentication process, the code is as follow:
class App extends Component {
constructor(props) {
super(props);
this.props.dispatch({
type: START_AUTH
});
}
On both popup/index.js and background/index.js I set the store communication channel:
//popup
import {Store} from 'react-chrome-redux';
import {Provider} from 'react-redux';
const proxyStore = new Store({
portName: 'my_app'
});
//background
import rootReducer from '../core/reducers';
import {wrapStore} from 'react-chrome-redux';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {Store} from 'react-chrome-redux';
import App from './components/App';
const store = createStore(rootReducer, {});
wrapStore(store, {
portName: 'my_app'
});
I've plenty of logging messages on the authentication process, but nothing seems to happen.
In core/ I have the common files, like reducers, action types, actions, etc, it's always translated from ES6 by webpack-babel.
Unfortunately it seems that React dev tools doesn't work on Chrome extensions to help debugging.
Any idea or any more information you need to help me to figure out what's happening and how to fix it?
This is for anyone who stumbles upon here in search for an answer. What #danielfranca posted is just symptom.
What actually happened is, there is an error thrown (in the background page) after the action is dispatched, so the action failed to finish. Refer to wrapStore.js or below (in case there is changes in their github).
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === DISPATCH_TYPE) {
const action = Object.assign({}, request.payload, {
_sender: sender
});
dispatchResponder(store.dispatch(action), sendResponse);
return true;
}
});
This line below, store.dispatch(action) returns a result. But if an error occurred during that (in the reducer), then you don't get the result.
dispatchResponder(store.dispatch(action), sendResponse);
So it sends back nothing (undefined) (refer to here). And in the Store.js, the dispatch function try to retrieve error key from undefined, which caused the error.
Because you are inspecting the popup/content page, you get this very vague error message.
If you inspect your background page, you will see the actual error.
I created a PR for displaying a more helpful error message. Hopefully it'll get merged.
The solution was much simpler than I expect.
The action names were not being exported, so the type being dispatched was actually undefined
Changing from:
const START_AUTH = "START_AUTH";
to:
export const START_AUTH = "START_AUTH";
Solved the problem.