Nextjs auth with Firebase not sharing cookie accross pages - javascript

I'm trying to create auth using Firebase and NextJS
I followed this tutorial: [https://colinhacks.com/essays/nextjs-firebase-authentication][1].
Whenever I access an SSR authenticated page the token is deleted from the front end, firebase gives an error stating FirebaseAuthError: First argument to verifyIdToken() must be a Firebase ID token string..
It seems like the cookie is deleted whenever SSR is run because Firebase see's the cookie as invalid. I've tried setting the cookie using js-cookie and using nookies.
I've tried different cookie frameworks for setting cookies, the NextJS native method of fetching cookies SSR and nookies for fetching cookies SSR.
auth.js
const AuthContext = createContext<{ user: firebaseClient.User | null }>({
user: null,
});
export function AuthProvider({ children }: any) {
const [user, setUser] = useState<firebaseClient.User | null>(null);
console.log("User is ", user)
useEffect(() => {
if (typeof window !== "undefined") {
(window as any).nookies = nookies;
}
return firebaseClient.auth().onIdTokenChanged(async (user) => {
if (!user) {
setUser(null);
// Cookies.set("token", "");
nookies.set(null, "token", "",);
return;
}
const token = await user.getIdToken();
// Cookies.set("token", token);
nookies.set(null, "token", token, {
path: "/",
encode: (v) => v,
});
setUser(user);
});
}, []);
useEffect(() => {
const handle = setInterval(async () => {
console.log(`refreshing token...`);
const user = firebaseClient.auth().currentUser;
if (user) await user.getIdToken(true);
}, 10 * 60 * 1000);
return () => clearInterval(handle);
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
}
export const useAuth = () => {
return useContext(AuthContext);
};
Authenticated page
export const getServerSideProps = async (ctx, req) => {
try {
const cookies = nookies.get(ctx);
// Fails here, never accepts token
const token = await firebaseAdmin.auth().verifyIdToken(cookies.token);
console.log("Token is:", token)
return {
props: {
data: "Worked!"
},
};
} catch (err) {
return {
props: {
data: "Firebase failed!"
},
};
}
};
const Revisions = ({ data, ctx }) => {
const { user } = useAuth();
return (
<Layout>
<Container>
{/* always returns no user signed in */}
<p>{`User ID: ${user ? user.uid : 'no user signed in'}`}</p>
<p>{data}</p>
</Container>
</Layout>
);
};
EDIT: Additional information
On the loginpage cookie is set on the front end. I can see this cookie in my Applications tab.
[![Application tab chrome showing token and value][2]][2]
I have verified that localhost with the correct port is permitted through the firebase console.
The token seems to get deleted at this line
const token = await firebaseAdmin.auth().verifyIdToken(cookie);
I have tried using JSON parse, both with JSON.stringify first and just straight JSON parsing the data.
[1]: https://colinhacks.com/essays/nextjs-firebase-authentication
[2]: https://i.stack.imgur.com/p8MES.png

Related

React GraphQL authentication flow with userContext

I have a React application which allows the user to login via a form then navigates to the home page. I need the App component to provide the user down to its children.
App.js
function App() {
const [token, setToken] = useState(null)
const userQuery = useQuery(USER) // Gets user from GraphQL, using token in the request context
...
useEffect(() => {
setToken(localStorage.getItem("user-token"))
}, [])
return (
<UserContext.Provider value={userQuery.data}>
...
</UserContext.Provider>
)
}
LoginForm.js
const LoginForm = () => {
...
const [login, result] = useMutation(LOGIN, {
onError: (error) => {
console.log("error :>> ", error)
setError(error.graphQLErrors[0].message)
},
})
useEffect(() => {
if (result.data) {
const token = result.data.login.value
localStorage.setItem("user-token", token)
navigate("/")
}
}, [result.data])
...
}
index.js
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem("user-token")
return {
headers: {
...headers,
authorization: token ? `bearer ${token}` : null,
},
}
})
When the login form submits the GraphQL login query is executed. Then when the result is received the token is set in local storage and it navigates back to the main page. The problem is that the app's user query receives a null user because the request wasn't sent with the new storage token. How can I get it to do this without refreshing the page?

next-auth JWT with custom backend access token never update till the client make hard refresh

I use CredentialsProvider to auth users with the Django backend
take the Credentials in authorize Callback
async authorize(credentials, req) {
try {
const respo = await axios.post(
"http://127.0.0.1:8000/auth/jwt/create/",
{
// hard coded for testing
phone_number: "123456789",
password: "123456",
}
);
const { access, refresh } = respo.data;
const respo1 = await axios.get(
"http://127.0.0.1:8000/auth/users/me/",
{
headers: { Authorization: "JWT " + access },
}
);
return { ...respo1.data, refresh: refresh, access: access };
} catch (e) {
return false;
}
},
request Django to create access, refresh token, and user info
then check for access token expire in JWT Callback
async jwt({ token, user, account, profile, isNewUser }) {
if (user) {
token.accessToken = user.access;
token.refreshToken = user.refresh;
return token;
}
if (isJwtExpired(token.accessToken)) {
const [newAccessToken, newRefreshToken] = await refreshToken(
token.refreshToken
);
if (newAccessToken) {
token = {
...token,
accessToken: newAccessToken,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000 + 2 * 60 * 60),
};
return token;
}
setAxiosAuthToken(null);
return {
...token,
exp: 0,
error: "RefreshAccessTokenError",
};
}
// token valid
return token;
},
session callback call setAxiosAuthToken to add Jwt in every Axios request
async session({ session, user, token }) {
session.accessToken = token.accessToken;
session.error = token.error;
setAxiosAuthToken(session.accessToken);
return session;
}
},
the problem is when the access token is refreshed setAxiosAuthToken never gets an update
and still request the old access token
I tried using getSession() in Axios interceptors.response but getSession() always returns null even in interceptors.request
also tried requesting session URL in Axios interceptors but respond with {}
Then I found an article for someone explaining the problem and offering a solution which is to use useSwr to keep the session up to date by pinging the session URL in interval
const { data, error } = useSwr(sessionUrl, fetchSession, {
revalidateOnFocus: true,
revalidateOnMount: true,
revalidateOnReconnect: true,
});
useEffect(() => {
const intervalId = setInterval(
() => mutate(sessionUrl),
(refreshInterval || 20) * 1000
);
return () => clearInterval(intervalId);
}, []);
return {
session: data,
loading: typeof data === "undefined" && typeof error === "undefined",
};
}
HOc for require auth views
export function withAuth(refreshInterval) {
return function (Component) {
return function (props) {
const { session, loading } = useAuth(refreshInterval);
if (typeof window !== undefined && loading) {
return null;
}
if (
(!loading && !session) ||
session?.error === "RefreshAccessTokenError"
) {
return (
<>
<button onClick={() => signIn()}>Sign in</button>
</>
);
}
return <Component session={session} {...props} />;
};
};
}
but the problem remains, session is updated with the latest access Token but Axios never gets updated till the next hard refresh

Firebase Cloud Messaging Web - onMessage not firing

I am trying to integrate FCM into my Next.js app. I have a hook to handle the messaging, but it isn't receiving my test messages. The code produces no errors either client side or server side, and logging shows that the onMessage callback is set, but after calling the API to send a test message, the client does not execute the callback.
_app.tsx
export default function MyApp({ Component, pageProps }: AppProps) {
initializeFirebase();
useMessaging();
return <Component {...pageProps} />;
}
hooks.ts
export function useMessaging() {
const [user] = useAuthState(getAuth());
const [token, setToken] = useState<string | null>(null);
useEffect(() => {
// VAPID_KEY is imported from another file
getToken(getMessaging(), { vapidKey: VAPID_KEY })
.then((token) => {
setToken(token);
onMessage(getMessaging(), (message) => {
// For testing, log that the message was received
console.log({ message });
});
})
}, []);
useEffect(() => {
if (user && token) {
// For testing, log the token
console.log({ token });
}
}, [user, token]);
}
/api/test.ts
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const serviceAccount = JSON.parse(
decodeURI((process.env as MoodGraphEnv).FIREBASE_SERVICE_ACCOUNT)
) as ServiceAccount;
if (getApps().length === 0) {
initializeApp({
credential: cert(serviceAccount),
databaseURL: 'https://my-app-123456-default-rtdb.firebaseio.com',
});
}
const message: Message = {
notification: {
title: 'Hello world!',
body: 'The notification worked!',
},
token:
// During testing I replace this with the token logged to the browser console
'token_goes_here',
};
getMessaging()
.send(message)
.then((messagingResponse) => {
console.log('Successfully sent message:', messagingResponse);
})
.catch((error) => {
console.log('Error sending message:', error);
});
res.status(200).json({});
}
I am not sure why the callback is not executed, so any help would be appreciated.

firebase reauthentication flow with Token in react js app not working properly

after an hour the user get disconnected from the firebase functions and gets an error.
the connection to firebase in the app work like this:
After the first connection via google, the token is sent to firebase functions.
after that a getIdToken(ture) to force a refresh in useEffect.
the token is saved in the state via mobX and every time a commend requires to send or get data from the data base it's passes the token to the firebase functions
I have noticed that I don't get a new token in .then(function (idToken) {...}
this is the error :
FirebaseAuthError: Firebase ID token has expired.
Get a fresh ID token from your client app and try again (auth/id-token-expired).
See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.
...
...
...
> errorInfo: {
> code: 'auth/id-token-expired',
> message: 'Firebase ID token has expired.
Get a fresh ID token from your client app and try again (auth/id-token-expired).
See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token.'
> },
> codePrefix: 'auth'
> }
Things that I have tried already:
is to separate the firebase.auth().currentUser.getIdToken(true).then() to a different useEffect().
call getIdToken() after I get an error from the firebase functions.
const UserSubscriber = () => {
//using mobX here
const { user } = useStores();
const token = user.store.token;
React.useEffect(() => {
if (!token.length || !firebase.auth().currentUser) return;
firebase.auth().currentUser.getIdToken(true).then(function (idToken) {
const decodedToken = jwt.decode(idToken, '', true);
if (!decodedToken.user_id) return;
const unsub = firebase.firestore().collection('users').doc(decodedToken.user_id).onSnapshot(docSnapshot => {
const data = docSnapshot.data();
//user.mergeData() is just to store data
if (!data) return user.mergeData({ noUser: true, token: idToken })
user.mergeData({ ...data, noUser: false, token: idToken })
});
return () => unsub();
}).catch(function (error) {
user.logOut();
});
}, [token, user]);
return useObserver(() => (
<div />
));
}
and in the backend
app.use(async (req, res, next) => {
try {
const decodedToken = await admin.auth().verifyIdToken(req.body.token);
let uid = decodedToken.uid;
req.uid = uid;
return next();
} catch (error) {
console.log(error);
return res.status(401).send();
}
});
I have tried firebase.auth().onAuthStateChanged(userAuth => {...}) (in side of the useEffect())
firebase.auth().onAuthStateChanged(userAuth => {
userAuth.getIdToken().then(function (idToken) {
const decodedToken = jwt.decode(idToken, '', true);
if (!decodedToken.user_id) return;
const unsub = firebase.firestore().collection('users').doc(decodedToken.user_id).onSnapshot(docSnapshot => {
const data = docSnapshot.data();
if (!data) return user.mergeData({ noUser: true, token: idToken })
user.mergeData({ ...data, noUser: false, token: idToken })
});
return () => unsub();
}).catch(function (error) {
user.logOut();
});
})
;
}

NuxtServerInit sets Vuex auth state after reload

I'm setting a basic authentication on a Nuxt project with JWT token and cookies to be parsed by nuxtServerInit function.
On login with email/password, works as intended, setUser mutation is triggered and the appropriate user object is stored in state.auth.user.
On reload, nuxtServerInit will get the jwt token from req.headers.cookies, call the GET method and identify user.Works like a charm.
Problem starts when I hit the /logout endpoint. state.auth.user is set to false and Im effectively logged out... but If I refresh, I'm logged in again with the previous user data. Even if my cookies are properly empty (on below code, both user and cookie are undefined after logout and refresh, as expected)
So I really don't get why is my state.auth.user is back to its initial value...
store/index.js
import Vuex from "vuex";
import auth from "./modules/auth";
import axios from "~/plugins/axios";
const cookieparser = process.server ? require("cookieparser") : undefined;
const END_POINT = "api/users";
const createStore = () => {
return new Vuex.Store({
actions: {
async nuxtServerInit({ commit, dispatch}, { req }) {
let cookie = null;
console.log(req.headers.cookie)
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie);
try {
cookie = JSON.parse(parsed.auth);
console.log("cookie", cookie)
const {accessToken} = cookie
const config = {
headers: {
Authorization: `Bearer ${accessToken}`
}
}
const response = await axios.get(`${END_POINT}/current`, config)
const user = response.data
console.log("user nuxt server init", user)
await commit('setUser', user)
} catch (err) {
// No valid cookie found
console.log(err);
}
}
}
},
modules: {
auth
}
});
};
export default createStore;
modules/auth.js
import axios from "~/plugins/axios";
const Cookie = process.client ? require("js-cookie") : undefined;
const END_POINT = "api/users";
export default {
state: {
user: null,
errors: {}
},
getters: {
isAuth: state => !!state.user
},
actions: {
login({ commit }, payload) {
axios
.post(`${END_POINT}/login`, payload)
.then(({ data }) => {
const { user, accessToken } = data;
const auth = { accessToken };
Cookie.set("auth", auth);
commit("setUser", user);
})
.catch(e => {
const error = e;
console.log(e);
commit("setError", error);
});
},
logout({ commit }) {
axios
.post(`${END_POINT}/logout`)
.then(({ data }) => {
Cookie.remove("auth");
commit("setUser", false);
})
.catch(e => console.log(e));
},
},
mutations: {
setUser(state, user) {
state.user = user;
},
setError(state, errors) {
state.errors = errors;
}
}
};
The way I logout my user is by creating a mutation called clearToken and commit to it in the action :
State :
token: null,
Mutations :
clearToken(state) {
state.token = null
},
Actions :
logout(context) {
context.commit('clearToken')
Cookie.remove('token')
}
This way, you token state revert back to null.

Categories

Resources