React GraphQL authentication flow with userContext - javascript

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?

Related

cognitoUser.refreshSession keeps refreshing user login

Im trying to refresh the jwt token with cognitoUser.refreshSession, everytime I log in it simply refreshes the login page and asks me to sign in again
I have written up some code from various examples I have found but still struggle to get it going. If anyone has any suggestions I would welcome them:
Code as follows:
import axios from 'axios';
import { Auth } from 'aws-amplify';
const httpClient = axios.create(
{ baseURL: process.env.VUE_APP_API_BASE_URL }
);
const refreshToken = async function() {
try {
const cognitoUser = await Auth.currentAuthenticatedUser();
const currentSession = await Auth.currentSession();
cognitoUser.refreshSession(currentSession.refreshToken, (err, session) => {
console.log('session', err, session);
return Auth.user.signInUserSession.idToken.jwtToken
});
} catch (e) {
console.log('Unable to refresh Token', e);
}
}
httpClient.interceptors.request.use(
config => {
const idToken = refreshToken()
config.headers.Authorization = idToken;
return config
}, error => Promise.reject(error))
export default httpClient;

Nextjs auth with Firebase not sharing cookie accross pages

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

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

How can I set my jwt as a cookie to prevent having to login on refresh?

I am attempting to store a JWT as a cookie to prevent my Axios call creating a new one each time and making me login every time the app is refreshed
I think I am on the right path using JS-Cookie and setting the cookie to my JWT provided by the API. However I am still redirected on login every refresh. How can I keep the authToken as my original JWT token?
import axiosAPI from 'axios';
import Cookies from 'js-cookie';
let authToken = null;
const axios = axiosAPI.create({
baseURL: `${baseURL}`
});
// User login
export const loginUser = (data) => {
return new Promise((resolve, reject) => {
axios.post(`${baseURL}/jwt-auth/v1/token`, data)
.then((res) => {
if (Cookies.get('token') == null) {
authToken = res.data.token;
} else {
Cookies.set('token', res.data.token);
authToken = Cookies.get('token');
}
// Adds the token to the header
axios.defaults.headers.common.Authorization = `Bearer ${authToken}`;
resolve(res.data);
})
.catch((error) => {
reject(error);
});
});
};
I have also tried this:
import axiosAPI from 'axios';
import Cookies from 'js-cookie';
const authToken = Cookies.get('token');
const axios = axiosAPI.create({
baseURL: `${baseURL}`
});
// User login
export const loginUser = (data) => {
return new Promise((resolve, reject) => {
axios.post(`${baseURL}/jwt-auth/v1/token`, data)
.then((res) => {
if (Cookies.get('token') === null) {
Cookies.set('token', res.data.token);
}
// Adds the token to the header
axios.defaults.headers.common.Authorization = `Bearer ${authToken}`;
resolve(res.data);
})
.catch((error) => {
reject(error);
});
});
};
which fails to log me in altogether

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