Running login mutation in utility function instead of component - reactjs

This question is probably a sign of my misunderstanding of the philosophy of Apollo/React/GraphQL, but I'm a bit stuck here.
I am writing the authentication logic for my single page app. I have a backend with a GraphQL API that authenticates users by returning a JWT access/refresh token pair. So, to login, I need to send a mutation to the GraphQL endpoint from my app.
My frontend uses Apollo Client. Now I understand I can run a mutation with useMutation, I understand how this works. But I also know that React Hooks need to be specified at the top of a component. I could have this useMutation call in a login component, but this feels like bad encapsulation.
The problem is that I want to have a separate login utility function that takes a username and password, sends the login mutation request to the backend, and returns the response.
From my understanding, I cannot use useMutation here, as I'm not in a React component. I can of course write a fetch call myself:
export const login = (username, password) => {
return fetch("/api", {
method: "POST",
...
}
but this feels unclean when I'm working within the Apollo Client ecosystem. Am I missing something, should I just roll ahead with these manual fetch calls, or am I misunderstanding the whole philosophy here?

As long as you have access to an instance of ApolloClient, you can call the mutate method on it directly. You can also pass the client in as a parameter, in which case you might find it helpful get the client instance through the useApolloClient hook. Alternatively, you can just export the client and then import it directly inside your function. Whatever approach you take, just make sure you're using the same instance of ApolloClient that you use elsewhere in your app to ensure you're updating the correct cache.
const client = new ApolloClient({...})
...
const { data, errors } = await client.mutate({...})

Related

How to perform an action/operation with nextjs API routes and server-side rendering

The next.js documentation is very clear about data fetching and how to create dynamic API routes. I wonder what is the correct way to trigger an action/method on the server-side. I.e. if I want to subscribe to a newsletter, there would be a button "Subscribe". When the user clicks the button, the users email address should be inserted into the database as a subscriber.
Based on the documentation I would create POST API endpoint by adding this to pages/api/subscribe.js:
function signup(emailAddress) {
// do arbitrary database connection work here and insert into subscriber table
}
export default function handler(req, res) {
if (req.method === "POST") {
signup(req.body.emailAddress)
res.json({ status: 'ok' })
}
}
Now, in the onClick() of the "Susbcribe" button, I could use fetch() to send that post request to /api/subscribe along with a JSON payload, but the documentation of nextjs explicitly states not to do that. Also, there is only getServerSideProps() that I could use in the next.js page code - but we do not want to signup a user upon page load, it is a single action/method after which the user should be redirected to a success page.
Additionally, it is obvious that in a client-side component I cannot import signup() since it contains server-side nodejs code.
Basically this question boils down to: "Okay, I can fetch data with getServerSideProps() without creating a REST API, but how do I perform server-side actions/invocations without creating and calling a REST API upon user interaction?".

Invoke Lambda function from Amplify-generated React App without using API Gateway

I used Amplify to generate a static website and the underlying React app. Initially I also generated an API endpoint but, because my lambda function may run over the API Gateway timeout limit (29 seconds), I need to invoke the lambda function directly from the generated React App, instead of going through API Gateway.
The code looks as follows, for the React page to authenticate using Cognito:
import Auth from '#aws-amplify/auth';
import { withAuthenticator } from 'aws-amplify-react';
import awsconfig from './aws-exports';
Auth.configure(awsconfig);
The above lines wrap the App (root) object and work as advertised. But since I do not want to use the API Gateway, how do I invoke the AWS Lambda function directly from React App?
The answers I could find talk about importing AWS etc, which seems to be in conflict with what we are trying to do here. I need to use the authenticated connection (which already works using the above code) when invoking lambda, so I cannot use generic invocation given in this example.
The Invoke API does not provide any examples as well.
Any advice is appreciated.
Note: if you do not need a response after your long running lambda, then consider API Gateways' Asynchronous Invocation
Amplify calls this approach "working with service objects".
To do this you'll have to ensure that the role Cognito gives your authenticated users includes permissions for lambda:invoke as well as any additional permissions needed within the function. I'll assume you can do that for now, however you can see the Role-Based Access Control documentation, or ask another question if not.
To access these roles within Amplify you need to use the Auth.currentCredentials function, which returns a promise with a credentials object, which can then be used on an aws-sdk client.
For example:
import Auth from '#aws-amplify/auth';
import Lambda from 'aws-sdk/clients/lambda'; // npm install aws-sdk
Auth.currentCredentials()
.then(credentials => {
const lambda = new Lambda({
credentials: Auth.essentialCredentials(credentials)
});
return lambda.invoke({
FunctionName: 'my-function',
Payload: JSON.stringify({ hello: world }),
});
})
You can see the full documentation for invoking lambdas on the AWS-SDK javascript documentation.
However you should be aware that the payload from API Gateway is constructed by AWS and includes much more information than just the body that the endpoint was called with, however when you invoke directly, all you'll get is the payload, so you'll have to build that payload object accordingly.

How to check if a user is logged in and how to save their information? (React)

So pretty much I made a login page communicates with a backend to login a user. Now I want to take their data and store it for all my other components to know that there is
A user logged in
Have access to the user's information
I figured based on past StackOverflow answers, that the best way to do this is using JWT and LocalStorage. But all the answer's I've encountered seem to use Redux. I'm not using Redux at all for my application, and have no clue how to use it, so I was wondering if there's a way to do it without redux.
You don't need Redux. You just have to store the JWT in localStorage. To do that, just use localStorage.setItem('token', data.token) when you receive the login success response from the API. It's that simple. You can read this article for more details. It's for React Redux applications but doesn't need Redux.
Then, assuming you are using react-router, you need two helper functions. The first will go on your protected routes and would look like this:
const requireAuth = (...) => {
if(!localStorage.getItem('token')) {
// go to login route
}
// stay on this route since the user is authenticated
}
The second goes on your unprotected routes and looks like this:
const verifyAuth = (...) => {
if(localStorage.getItem('token')) {
// go to your dashboard or home route
}
// stay on this route since the user is not authenticated
}
Keep in mind that you have to refresh the token according to the server expiration time. A good approach would be to create a call to the server asking if the token is still valid.

Using an auth function on an onEnter route vs using a high order function and are both methods really secure?

Lets take a system where a jwt token is saved to local storage upon login and now we check for the token existence to allow a user access to a protected route.
We're using react-router-redux and bundling everything with webpack and our code is in the client/src folder as apposed to the server folder.
So the protected route with our auth check would look something like the following (let's say we send our routes only on demand) :
import { push } from 'react-router-redux'
function requireAuth(store) {
const check = localStorage.jwt
check ? store.dispatch(push('/dashboard')) : store.dispatch(push('/'))
}
export default (store) => ({
path: 'dashboard',
onEnter: requireAuth(store),
getComponent (nextState, cb) {
require.ensure([], (require) => {
const Dashboard = require('./containers/DashboardContainer').default
cb(null, Dashboard)
}, 'dashboard')
}
})
And for completion our index routes file:
import Login from './login'
import Dashboard from './Dashboard'
import Logout from './Logout'
export const createRoutes = (store) => ({
path: '/',
indexRoute: Login,
childRoutes: [
Dashboard(store), //Our protected route
Logout(store)
]
})
export default createRoutes
Putting a side man in the middle attacks and the inherint problems with the nature of javascript crypto methos as discussed here:
SPA best practices for authentication and session management
I would like to focus on the above illustrated method of a function placed on the onEnter to allow access:
If I understand correctly the above example is bundled and sent to the client, Now I might be wrong and the route definitions aren't actually being sent in the bundle.
So I guess the question is: does a user has access or can see our above route definitions by default - if so how do we prevent it?
Also in regards to our auth check in the example, which I've seen used and simply redirects if a token is not present in the local storage, is that really sufficient prevention?
Comparing the above example to using a high order function to wrap our protected component with, so we can preform auth checks in the life cycle methods WillMount and WillRecieveProps as you can see here for example.
I would think that both approaches would suffer from being exposed to the client. Am I right?
What other caveats am I missing regarding each approach?
In my opinion you shouldn't store you JWT token in localStorage, instead you should keep it as a cookie with httpOnly and secure flags as true. So that your client script can't get your JWT token. Also man in the middle attacks are not possible since you are sending your cookie on https only.
You could have a api endpoint which checks if the client has JWT and if it is valid. So that you can do your redirects on onEnter function wherever you need.
EDIT
If I understand correctly the above example is bundled and sent to the
client, Now I might be wrong and the route definitions aren't actually
being sent in the bundle. So I guess the question is: does a user has
access or can see our above route definitions by default - if so how
do we prevent it?
Yes your whole application is being sent to client with your all client code, so anyone can see what you have written if they want to.
Also in regards to our auth check in the example, which I've seen used
and simply redirects if a token is not present in the local storage,
is that really sufficient prevention?
Not really, if you have a invalid token, you redirect your user to page and if this page does some fetching from server, you will get 401 error and you will have to check 401 errors so that you can redirect user to login. Which doesn't seem efficient or clear.
Comparing the above example to using a high order function to wrap our
protected component with, so we can preform auth checks in the life
cycle methods WillMount and WillRecieveProps as you can see here for
example.
All of them are being exposed to client except httpOnly flag true cookie.
Hope it helps.

React/Redux server side rendering initial state

I have a React/Redux application that keeps track of a user's authenticated state in local storage. The application is also set up to use server side rendering. I'm running into issues when rendering the initial application state. My server creates a new store and emits the SET_INITIAL_STATE action. This initial action on the client side reads localStorage and passes the authenticated information on to my reducers. The server, however, has no knowledge of this logged in state since I'm using stateless JWT located in local storage for authentication.
Since the server and client are out of sync at this point, I'm getting this error:
React attempted to reuse markup in a container but the checksum was invalid. This generally means that you are using server rendering and the markup generated on the server was not what the client was expecting. React injected new markup to compensate which works but you have lost many of the benefits of server rendering. Instead, figure out why the markup being generated is different on the client or server:
Which makes sense, because the server is trying to render an unauthenticated state.
What is the accepted standard or practice for setting this initial state that relies solely on something the client has access to?
I found that the OP is right, that cookie storage is the best option. If you're using react, redux, and express for a universal app the option that worked for me was https://github.com/eXon/react-cookie.
Essentially:
In you server side code you can use:
import cookie from 'react-cookie';
function handleRender(req, res) {
cookie.setRawCookie(req.headers.cookie);
// Create new Redux store instance
const store = createStore(rootReducer, {
username: cookie.load('username') || '',
accessLevel: cookie.load('accessLevel') || userRoles.public,
});
//other react/redux/express related code
}
In your react applications, inside the components, you can just save the cookies directly:
import cookie from 'react-cookie';
//first parameter is anything you want, second is the data you want to save
cookie.save( 'username', username );
cookie.save('accessLevel', accessLevel);
Further if you want to store an object, as I did, you can pass in JSON.stringify(your object). cookie.load will automatically parse it for you.
In my code I called cookie.save from the actions, but you can call it directly in components or any abstraction you have.
I found a working solution.
The trick, unfortunately, is to store the authenticated state in a cookie so the session state gets sent to the server automatically upon request.

Resources