I have a Apollo client and server with a React app in which users can log in. This is the Apollo server mutation for the login:
loginUser: async (root, args) => {
const theUser = await prisma.user.findUnique({
where: {email: String(args.email)},
});
if (!theUser) throw new Error('Unable to Login');
const isMatch = bcrypt.compareSync(args.password, theUser.password);
if (!isMatch) throw new Error('Unable to Login');
return {token: jwt.sign(theUser, 'supersecret'), currentUser: theUser};
},
This returns a JWT and the user that's logging in.
In my React app I have a login component:
// Login.tsx
const [loginUserRes] = useMutation(resolvers.mutations.LoginUser);
const handleSubmit = async (e) => {
e.preventDefault();
const {data} = await loginUserRes({variables: {
email: formData.email,
password: formData.password,
}});
if (data) {
currentUserVar({
email: data.loginUser.currentUser.email,
id: data.loginUser.currentUser.id,
loggedIn: true,
});
window.localStorage.setItem('token', data.loginUser.token);
}
};
This function passes the form data to the LoginUser mutation which returns data if authentication is successful. Then I have a reactive variable called currentUserVar I store the email and id of the user in there so I can use it throughout the application. Finally I store the JWT in a LocalStorage so I can send it for authorization:
// index.tsx
const authLink = setContext((_, {headers}) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
});
Everything is working, except for the fact that if a user refreshes the user data is gone and they have to log in again, which is of course quite annoying.
So I was hoping to get some advice on how to persist the data, perhaps using Apollo? I suppose I could add a checkbox with a remember me function that stores the email and id in the LocalStorage and when the app initiates check if there's user data in the LocalStorage and than use that, but I was wondering if there's a better/other way to do this.
When it comes to the login problem , you have set the headers on your every single request , but did you pass a fuction to the ApolloServer constructor that checks the headers from every single request ? Something like this:
const server=new ApolloServer({
typeDefs,
resolvers,
context:async({req})=>{
const me=getMe(req)
return {
models,
me,
process.env.SECRET
}
}
})
const getMe = async req => {
const token = req.headers['x-token'];
if (token) {
try {
return await jwt.verify(token, process.env.SECRET);
} catch (e) {
throw new AuthenticationError(
'Your session expired. Sign in again.',
);
}
}
};
As for the data persistence part of the question , you have to use setItem to persist the token in the locatStorage.
Related
I'm writing back on nestjs/mongodb and front on reactjs. And use graphql between them. I had some needed info in headers, or I passed it through variables in query.
Is it cheaper to pass it through variables or through context?
When user logs in, I'm setting headers: filialIds
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem(${localStorageAppPrefix}.token`);
return {
headers: {
...headers,
filialIds: `${localStorage.getItem(`${localStorageAppPrefix}.filialIds`) ?? ''}`,
authorization: token ? `Bearer ${token}` : '',
},
};
});
export const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
});`
When user query smth, I'm checking his filialIds and role in Guard
`
#Injectable() export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const ctx = GqlExecutionContext.create(context);
const requiredRoles = this.reflector.getAllAndOverride<UserRoles[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const queryFilialIds =
safeJSONParse(ctx.getContext()?.req?.headers?.filialids ?? '') ?? [];
const { roles, filialIds } = ctx.getContext()?.req?.user ?? {};
const hasRequiredFilials = filialIds?.every(
(filialId) => queryFilialIds.indexOf(filialId) !== -1,
);
const hasRequiredRoles = requiredRoles.some(
(role) => roles?.indexOf(role) !== -1,
);
return hasRequiredRoles || hasRequiredFilials;
}
}`
But I also need the access to filialIds and role in service, like here:
async getCount(context): Promise<number> {
const filialIds =
JSON.parse(context?.req?.headers?.filialids ?? '') ?? [];
return this.userModel.countDocuments({ filialIds: { $in: filialIds } });
}
So the question is: Should I use context or pass it from graphql query like here:
const { data } = useQuery(GET_USER, {
variables: { filialIds: filialIds ?? [] },
skip: !filialIds?.length,
});
There are many ways to solve this problem. I presume that filialIds is some sort of affiliation id?
If the affiliation id is an attribute of the user then you could include it in your JWT token or just append it to the user object whenever you check the JWT. Furthermore if it's a user attribute then there's no need to send it from the client to server at all.
If the affiliation id is an attribute of the site in a multi-tenant situation then a header makes perfect sense - not however that one site could potentially spoof another
If most or many of your resolvers need this variable then having it as a query variable seems tedious.
I am trying to implement an React solution with Strapi as backend where authorization is done using JWT-keys. My login form is implemented using the function below:
const handleLogin = async (e) => {
let responsekey = null
e.preventDefault();
const data = {
identifier: LoginState.username,
password: LoginState.password
}
await http.post(`auth/local`, data).then((response) => {
setAuth({
userid: response.data.user.id,
loggedin: true
})
responsekey = response.data.jwt
setLoginState({...LoginState, success: true});
sessionStorage.setItem('product-authkey', responsekey);
navigate('/profile');
}).catch(function(error) {
let result = ErrorHandlerAPI(error);
setLoginState({...LoginState, errormessage: result, erroroccurred: true});
});
}
The API-handler should return an Axios item which can be used to query the API. That function is also shown below. If no API-key is present it should return an Axios object without one as for some functionality in the site no JWT-key is necessary.
const GetAPI = () => {
let result = null
console.log(sessionStorage.getItem("product-authkey"))
if (sessionStorage.getItem("product-authkey") === null) {
result = axios.create(
{
baseURL: localurl,
headers: {
'Content-type': 'application/json'
}
}
)
} else {
result = axios.create({
baseURL: localurl,
headers: {
'Content-type': 'application/json',
'Authorization': `Bearer ${sessionStorage.getItem("product-authkey")}`
}
})
}
return result
}
export default GetAPI()
However, once the user is redirected to the profile page (on which an API-call is made which needs an JWT-key), the request fails as there is no key present in the sessionStorage. The console.log also shows 'null'. If I look at the DevTools I do see that the key is there... And if I refresh the profile page the request goes through with the key, so the key and backend are working as they should.
I tried making the GetAPI function to be synchronous and to move the navigate command out of the await part in the handleLogin function, but that didn't help.
Does someone have an idea?
Thanks!
Sincerely,
Jelle
UPDATE:
Seems to work now, but I need to introduce the getAPI in the useEffect hook, I am not sure if that is a good pattern. This is the code of the profile page:
useEffect(() => {
let testapi = GetAPI()
const getMatches = async () => {
const response = await testapi.get(`/profile/${auth.userid}`)
const rawdata = response.data.data
... etc
}, [setMatchState]
export default GetAPI() this is the problematic line. You are running the GetApi function when the module loads. Basically you only get the token when you visit the site and the js files are loaded. Then you keep working with null. When you reload the page it can load the token from the session storage.
The solution is to export the function and call it when you need to make an api call.
I am using with-iron-session for authentication in my NextJS app however I'm not able to access the session cookie when I make API calls in my getServerSideProps() function. The API route is unable to get the session cookie used for authentication:
Session creation:
// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
import { withIronSession } from "next-iron-session";
export default function withSession(handler) {
return withIronSession(handler, {
password: process.env.PASSWORD_HASH,
cookieName: "MYCOOKIE",
cookieOptions: {
// the next line allows to use the session in non-https environements like
// Next.js dev mode (http://localhost:3000)
secure: process.env.NODE_ENV === "production",
httpOnly: false,
},
});
}
My getServerSideProps call:
export const getServerSideProps = withSession(async ({ req, res }) => {
const user = req.session.get("user");
if (!user) {
return {
redirect: {
permanent: false,
destination: "/"
},
props: {}
};
}
// I've replaced 'include' with 'same-origin' but it didn't make a difference
const watchRes = await fetch('/watch',{credentials: 'include'});
const watch = await watchRes.json();
return{
props: {
user,
watch
}
}
}
the api route:
// I've added await before the (req,res) but that was just guessing at this point
export default withSession((req, res) => {
const user = req.session.get("user");
if(user){
res.send("Good");
}else{
res.status(403).end();
}
}
While being logged in and I go to my localhost/api/watch route, it shows "Good" but when I try the fetch request I get the 403.
try to add headers of the getServerSideProps request to the second call.
const headers = req.headers
const watchRes = await fetch('/watch',{headers});
in this way you have injected the cookies
I'm trying to get sessions to work with a React front-end and an express + connect-mongo using MongoStore back-end.
Handle Register Function
async function handleRegister(evt){
//Prevent default form redirect.
evt.preventDefault();
//Create a new user objec to pass into axios
const user = {
username: username,
password: password
}
//Send axios post request to nodeJS API.
await axios.post("http://localhost:5000/users/register",user)
.then((res) => {
history.push({
pathname: '/',
state: res.data
});
})
.catch((err) => {
console.log(err);
});
//Push react history back to index page.
}
Handle Login function
const handleLogin = async (evt) => {
//Prevent default form submission
evt.preventDefault();
const loginDetails = {
username: username,
password: password,
}
//send login request to api
await axios.post('http://localhost:5000/users/login', loginDetails)
.then((res) => {
})
.catch((err) => {
})
}
I'm stuck on trying to figure out how to make the data be sent back to react after either of the above functions. In the register function I've sent back the res.data which contains the session. See blow route for express
router.post('/register', async (req, res) => {
//Destructure req.body.
const {username,password} = req.body;
//hash password.
const hashedPassword = await hashPassword(password);
//Create new user to store in mongodb.
const newUser = {
username: username,
password: hashedPassword
}
//Create new user document
await User.create(newUser, (err, newlyAddedUser) => {
if(err){
console.log(err);
}else{
req.session.username = newlyAddedUser.username;
console.log(req.session);
res.send(req.session);
}
})
});
With the console.log(req.session) it outputs the cookie and the username I added in the session itself.
Should I make a user object on the react side and store the username and password inside?
Should I be passing back the session itself to the route with history.push({ pathname: '/',state: res.data});
How can I verify that the session is valid for the user using connect-mongo?
I spent 10 minutes trying to understand what is your goal. Didn't find.
But whatever you need to use a jsonwebtoken if you want to verify that the session is valid like you said
Enjoy https://jwt.io/
https://www.npmjs.com/package/jsonwebtoken
I wouldn't store the session in the History state API like you do.
history.push({
pathname: '/',
state: res.data
});
You better use a sessionStorage and/or localStorage. The name just talks by itself.
Give me one point please
I feel like there's probably just a fundamental gap in my knowledge, as I'm new to Apollo Client. But I've perused Stack Overflow, GitHub issues, and the Google for obvious solutions to an issue I'm running into and haven't found any.
Basically I have the following Apollo Client setup (simplified):
const auth = new Auth()
const authMiddleware = new ApolloLink((operation, forward) => {
const authToken = auth.getToken().access_token
console.log(authToken)
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: authToken ? `Bearer ${authToken}` : ''
}
}))
return forward(operation)
})
const cache = new InMemoryCache()
const errorLink = onError(({ forward, graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ extensions, locations, message, path }) => {
if (extensions.code === 'access-denied') {
auth.refresh()
.then(() => {
console.log(`new access token: ${auth.getToken().access_token}`)
return forward(operation)
}).catch((error) => {
handleLogout(error)
})
}
})
}
})
const handleLogout = (reason) => {
auth.logout()
}
const httpLink = new HttpLink({ uri: '' })
const client = new ApolloClient({
cache: cache,
link: ApolloLink.from([
errorLink,
authMiddleware,
httpLink
])
})
And I have a simple query:
client.query({
query: Queries.MyQuery
}).then((response) => {
console.log(response)
}, (error) => {
console.log(error)
})
The client successfully executes the query if there's a valid OAuth access token the first time it runs. If, however, I expire the access token on our OAuth server and then try to execute the query, it does not complete successfully.
When debugging, I can see what's going on:
authMiddleware adds the old access token properly to the request header.
The request fails because the token is no longer valid. This is handled property by errorLink.
errorLink also successfully retrieves a new access token and returns forward(operation).
authMiddleware gets called again, adds the new access token, and returns forward(operation).
This is where things break down. The query never re-executes. If I manually refresh the page to re-execute the query, it uses the new access token and completes successfully.
From reading the docs, it sounds like the way I've set it up should work, but obviously I'm doing something incorrectly.
I was able to piece together what was going on by digging into a variety of sources. It was confusing mostly because a lot of devs have struggled with this in the past (and still seem to be), so there's a plethora of outdated solutions and posts out there.
This GitHub issue was the most useful source of information, even though it's attached to a repository that's now deprecated. This Stack Overflow answer was also helpful.
I spent some time going down the path of using a utility method to turn the promise into an Observable, but this is no longer required if you use fromPromise.
Here's the solution I ended up with that works with Apollo Client 3.2.0:
const authLink = new ApolloLink((operation, forward) => {
const authToken = auth.getToken().access_token
console.info(`access token: ${authToken}`)
operation.setContext(({ headers }) => ({
headers: {
...headers,
authorization: authToken ? `Bearer ${authToken}` : ''
}
}))
return forward(operation)
})
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
const firstGraphQLError = graphQLErrors[0]
if (firstGraphQLError.extensions.code === 'access-denied') {
let innerForward
if (!isRefreshing) {
isRefreshing = true
innerForward = fromPromise(
auth.refresh()
.then(() => {
const authToken = auth.getToken().access_token
console.info(`access token refreshed: ${authToken}`)
resolvePendingRequests()
return authToken
})
.catch(() => {
pendingRequests = []
// Log the user out here.
return false
})
.finally(() => {
isRefreshing = false
})
).filter(value => Boolean(value))
} else {
innerForward = fromPromise(
new Promise(resolve => {
pendingRequests.push(() => resolve())
})
)
}
return innerForward.flatMap(() => {
return forward(operation)
})
} else {
console.log(`[GraphQL error]: Message: ${firstGraphQLError.message}, Location: ${firstGraphQLError.locations}, Path: ${firstGraphQLError.path}`)
}
}
if (networkError) {
console.log(`[Network error]: ${networkError}`)
}
})
const client = new ApolloClient({
cache: new InMemoryCache(),
link: from([
errorLink,
authLink,
new HttpLink({ uri: '' })
])
})
This solution also handles multiple concurrent requests, queuing them up and requesting them once the access token has been refreshed.