Updating headers of ApolloClient after Firebase login - javascript

I'm having a problem with sending an authorization header with a graphql request when a user signs up with my react app.
My flow is:
User signs up with Firebase, react app receives id token.
User is redirected to another page where they can fill out more information.
User clicks submit, a request is sent via graphql (Apollo) to custom backend to create user.
The problem is when the user clicks submit on the secondary sign up page to enter their name, the request that is sent to the backend does not contain the authorization header. If I reload that page before clicking submit (this is after firebase sign up was successful), then it works as expected.
index.js:
const token = localStorage.getItem(AUTH_TOKEN);
const client = new ApolloClient({
link: new HttpLink({
uri: 'http://localhost:9000/graphql',
headers: {
authorization: token ? `Bearer ${token}` : ''
}
}),
cache: new InMemoryCache()
});
App.js:
componentWillMount() {
const _this = this;
firebaseApp.auth().onAuthStateChanged((user) => {
if (user) {
console.log('AUTH STATE CHANGED', user);
// If logged in...
_this.setState({ loggedin: true });
user.getToken()
.then((result) => {
localStorage.setItem(AUTH_TOKEN, result);
});
} else {
// If not logged in...
_this.setState({ loggedin: false });
}
});
}
SignUp.js (this is where the user can authenticate with firebase):
handleSubmit(e) {
e.preventDefault()
const email = this.state.email.trim()
const password = this.state.password.trim()
if (isEmail(email)) {
firebaseApp
.auth()
.createUserWithEmailAndPassword(email, password)
.then(() => {
browserHistory.push('/signupcontinued');
})
.catch((error) => {
// Handle Errors here.
const errorMessage = error.message
alert(`errorMessage: ${ errorMessage}`)
})
} else {
alert('Email Address in not valid')
}
}
SignUpContinued.js (where the user enters their name before sending create user request to custom backend):
const SignUpMutation = gql`
mutation CreateUser($userInput: UserInput!) {
user {
create(organizationId: 3, userInput: $userInput) {
id
firstName
lastName
email
organizationId
balance
}
}
}
`
class SignupContinued extends Component {
render() {
let firstname;
let lastname;
return (
<div>
<Mutation mutation={SignUpMutation}>
{(signup, { data }) => (
<div>
<form
onSubmit={e => {
e.preventDefault();
const userInput = {
firstName: firstname.value,
lastName: lastname.value,
email: (firebaseApp.auth().currentUser) ? firebaseApp.auth().currentUser.email : ''
}
signup({ variables: {
userInput
}}).then(() => {
browserHistory.push('/home')
});
firstname.value = '';
lastname.value = '';
}}
>
<input
placeholder='Enter First name'
ref={node => {
firstname = node;
}}
/>
<input
placeholder='Enter Last name'
ref={node => {
lastname = node;
}}
/>
<button type='submit'>Submit</button>
</form>
</div>
)}
</Mutation>
</div>
)
}
}
Am I correctly redirecting the user so that react reloads (and the ApolloClient updates its headers? Or is the issue something to do with my .then functions and onAuthStateChanged isn't done running before the redirect?
Thanks!

Apollo client gets token data from localStorage before firebase set token to localstorage. you should refresh apollo header after firebase setup

Related

How can I complete authentication and create private routes after creating the token and sending it to the front-end?

I'm having a problem trying to learn authentication with JWT, cause I don't know what to do after checking the username and password with bcrypt and creating the token.
I was thinking that maybe it's because I don't understand how headers works, so I hope someone can help me with my problem.
Here's the relevant parts of my code:
Index (back-end where i'm generating the token and sending it to the front-end):
app.post('/login', (req, res) => {
usersModel.users.findOne({username: req.body.username}, (err, user) => {
if(err) {
res.json('ERRO!: ' + err)
}
else {
if(user!=null) {
bcrypt.compare(req.body.password, user.password, (err, response) => {
if(err) {
res.json('ERRO!: ' + err)
}
else {
if(response==true) {
const token = jwt.sign({
id: user._id
}, SECRET, { expiresIn: '30000'})
res.json({message: 'Passwords batem!', token})
}
if(response==false) {
res.json('Passwords não batem!')
}
}
})
}
}
})
})
Login.js:
import React from "react"
import {Link} from "react-router-dom" import { useState, useEffect } from "react";
export default function Login() {
const [username, setUsername] = useState("") const [password, setPassword] = useState("") const [listOfUsers, setListOfUsers] = useState([])
const handleSubmit = () => {
fetch('http://localhost:3001/login', {
method: 'POST', headers: {
'Content-Type': 'application/json'
}, body: JSON.stringify({username, password})
}).then((response) => response.json()).then((res) => {
console.log(res)
})
}
return (
<div className="logindiv">
<h1>Login: </h1>
<div className="inputlogindiv">
<input className="inputlogin" type="text" placeholder="Username..." onChange={(e) => {
setUsername(e.target.value)
}}></input>
<input className="inputlogin" type="text" placeholder="Password..." onChange={(e) => {
setPassword(e.target.value)
}}></input>
</div>
<br></br> <br></br>
<div className="logindivbuttons">
<button className="buttonregisterlogin" onClick={handleSubmit}>Login</button> <button className="buttonregisterlogin">Register</button>
</div>
<br></br>
</div>
)
}
After I send the token to the front-end it shows the token and the message that the password matches, but I need to know how to acess this token or send it to the header when I fetch on the front-end to complete the authentication logic.
I have to create private routes using JWT, if someone is able to help-me, please, do it, cause I'm really having a bad time lately.
First of all after successful call to api, you need to save your token somewhere, it can be localstorage for example. You will need it to pass token to your requests and check is user authenticated. After saving token somewhere you need to make a redirect to some protected route, so the user wouldn't stay on login page.
Make sure that you also check if user authenticated on login\registration pages, so user couldn't see them when he logged in.
Here is a link about how to configure private routes in react-router-dom
Hope it helped.

Persist the login state in Next js using localstorage

I'm writing an authentication application in Next Js (v12.2.5). The application also uses React (v18.2.0).
The problem is with persisting the authentication state. When the browser is refreshed, the login session is killed. Why is this happening even though I am getting and setting the token in the local storage. I would like to persist the login session to survive browser refresh and only kill the session when the user logs out.
Application flow
Users authenticates through a login form which calls a Spring API to fetch credentials from a mySQL database. Everything works as expected. The user is able to login, conditional rendering and route protection all function as expected.
Persisting the session relies on the localstorage API to store the JWT token once the user logs in. The Chrome browser console shows that the token is successfully set and stored throughout the authentication process. The get method for getting the initial token also seems to work.
Background
There are several questions on SFO that cover this topic but most seem to cover the use of cookies like this example. This question covers localstorage, but simply says to wrap the token get method is useEffect which doesn't address the actual questions and problems I'm having.
This example also covers localstorage but takes a different approach, using useReducer where my approach is trying to use use Effect. I'm open to restructure my whole application to use useReducer if this is the correct way, but first I want to make sure I understand if I'm taking the right approach.
I also suspect there is a difference between persisting the user state using React and Next. From researching, the difference seems to be in the way Next also includes SSR which may explain why I'm not able to persist the state in Next?
Application code
auth-context.js
const AuthContext = React.createContext({
token: '',
admintoken: '',
isLoggedIn: false,
isAdmin: false,
login: (token) => { },
adminAccess: (admintoken) => { },
logout: () => { },
});
export const AuthContextProvider = (props) => {
useEffect(()=> {
if(typeof window !== 'undefined') {
console.log('You are on the browser');
initialToken = localStorage.getItem('token');
console.log("InitialToken set "+ initialToken);
} else {
initialToken = localStorage.getItem('token');
console.log('You are on the server and token is ' + initialToken);
}
},[AuthContext])
const [token, setToken] = useState(initialToken);
const [admintoken, setAdminToken] = useState(initialToken);
const userIsLoggedIn = !!token;
const userHasAdmin = !!admintoken;
const loginHandler = (token) => {
setToken(token);
localStorage.setItem('token', token);
console.log("token stored " + token);
};
const logoutHandler = () => {
setToken(null);
localStorage.removeItem('token');
};
const adminTokenHandler = (admintoken) => {
setAdminToken(admintoken);
}
const contextValue = {
token: token,
admintoken: admintoken,
isAdmin: userHasAdmin,
isLoggedIn: userIsLoggedIn,
adminAccess: adminTokenHandler,
login: loginHandler,
logout: logoutHandler,
};
return (
<AuthContext.Provider value={contextValue}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthContext;
ProtectRoute.js
const ProtectRoute = ({ children }) => {
const authCtx = useContext(AuthContext);
const isLoggedIn = authCtx.isLoggedIn;
if (!isLoggedIn && typeof window !== 'undefined' && window.location.pathname == '/') {
return <HomePage />;
} else {
if (!isLoggedIn && typeof window !== 'undefined' && window.location.pathname !== '/auth') {
return <RestrictedSection />;
}
else {
console.log("User logged in");
// return <RestrictedSection />;
return children;
}
}
}
export default ProtectRoute;
Authform.js (login page)
const AuthForm = () => {
const emailInputRef = useRef();
const passwordInputRef = useRef();
const [isLoading, setIsLoading] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
const router = useRouter();
const authCtx = useContext(AuthContext);
const submitHandler = (event) => {
event.preventDefault();
const enteredEmail = emailInputRef.current.value;
const enteredPassword = passwordInputRef.current.value;
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
var urlencoded = new URLSearchParams();
urlencoded.append("username", enteredEmail);
urlencoded.append("password", enteredPassword);
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: urlencoded,
redirect: 'follow'
};
fetch(API_LOGIN_URL, requestOptions)
.then(async (res) => {
setIsLoading(false);
if (res.ok) {
return res.json();
} else {
const data = await res.json();
let errorMessage = 'Authentication failed!';
throw new Error(errorMessage);
}
})
.then((data)=> {
authCtx.login(data.access_token);
router.replace('/');
const processedData = JSON.stringify(data);
console.log("Admin status "+ processedData);
for(let i = 0; i < processedData.length; i++) {
if(processedData.includes("ROLE_SUPER_ADMIN")) {
console.log("Found Admin");
authCtx.adminAccess(true);
}
if(processedData.includes("ROLE_USER")) {
console.log("Found User");
break;
}
else {
console.log("Not Found");
}
}})
.catch((err) => {
alert(err.message);
});
};
return (
<section className={classes.auth}>
<h1>Login</h1>
<form onSubmit={submitHandler}>
<div className={classes.control}>
<label htmlFor='email'>Your Email</label>
<input type='email' id='email' required ref={emailInputRef} />
</div>
<div className={classes.control}>
<label htmlFor='password'>Your Password</label>
<input type='password' id='password' required ref={passwordInputRef} />
</div>
<div className={classes.actions}>
{!isLoading && <button>Login</button>}
{isLoading && <p>Sending request</p>}
</div>
</form>
</section>
);
};
export default AuthForm;
Issue 1
From your code in auth-context.js, you are calling useEffect inside a condition.
if(typeof window !== 'undefined') {
console.log('You are on the browser');
useEffect(()=> {
initialToken = localStorage.getItem('token');
console.log("InitialToken set "+ initialToken);
})
} else {
useEffect(()=> {
initialToken = localStorage.getItem('token');
console.log('You are on the server and token is ' + initialToken);
})
}
You SHOULD NOT call your useEffect(or any other hook) inside conditions, loops and nested functions.
Doc reference: https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level.
Consider moving your conditions code inside the hook.
useEffect(()=> {
if(condition)
{run your localstorage related logic here...}
})
Issue 2
I think you should consider adding a dependency array to your useEffect hook because getting your token on every rerender seems quite expensive.
useEffect(()=> {
if(condition)
{run your localstorage related logic here...}
},[])
Still, its just a suggestion, as I don't know your code in much depth.
Issue 3
The initial token is not getting set in the use effect.
Kindly add setToken(initialToken) in the useEffect after initial token assignment.
initialToken = localStorage.getItem('token');
setToken(initialToken);
The main issue is with you trying to run serverside code on the fronted:
useEffect(()=> {
if(typeof window !== 'undefined') {
console.log('You are on the browser');
initialToken = localStorage.getItem('token');
console.log("InitialToken set "+ initialToken);
} else {
initialToken = localStorage.getItem('token');
console.log('You are on the server and token is ' + initialToken);
}
},[AuthContext])
The above part of the code will always run on the front end(so you don't need the if part).
If you want to clear your concepts on what part of the code will work on the server and what part will run on the client, kindly refer to these documentations:
SSR: https://nextjs.org/docs/basic-features/data-fetching/get-server-side-props
SSG: https://nextjs.org/docs/basic-features/data-fetching/get-static-props
ISR: https://nextjs.org/docs/basic-features/data-fetching/incremental-static-regeneration

client fetch error on live website built using next.js

Hi I have tried all things possible to find out what could be causing above error on my live website built using NEXTJS.
I have noticed that this error happens whenever I reload the website.
I also notice that whenever I try to login using userName and password, I am able to do that without any errors in local host and also using https://codesandbox.io. But on the live site I get a server error "problem with the server configuration.".
when I scroll further on my developer tools I find the following additional information.
Unexpected token < in JSON at position 0 {error: {…}, path: "session", message: "Unexpected token < in JSON at position 0"
I have added the following environment variables in vercel
NEXTAUTH_URL = https://****.vercel.app/
MONGODB_URI = mongodb+srv://****#cluster0.9kc5p.mongodb.net/*****?retryWrites=true&w=majority
my [...nextauth].js file is as below
import NextAuth from "next-auth";
import CredentialsProviders from "next-auth/providers/credentials";
import { verifyPassword } from "../../../lib/hashedPassword";
import clientPromise from "../../../lib/mongodb";
export default NextAuth({
session: {
strategy: "jwt"
} /* check other providers you may add database etc */,
providers: [
CredentialsProviders({
/* authorize will be called when it receives incoming login req */
async authorize(credentials) {
const client = await clientPromise;
const db = client.db();
/* check if we have user or email */
const usersCollection = await db.collection("users");
const user = await usersCollection.findOne({
$or: [
{ email: credentials.email },
{ userName: credentials.userName }
]
});
if (!user) {
throw new Error("No user found");
}
const isvalid = await verifyPassword(
credentials.password,
user.password
);
if (!isvalid) {
throw new Error("password is invalid");
}
return {
email: user.email
};
}
})
]
});
my login page is as below
import Button from "../../UI/Button/Button";
import Input from "../../UI/Input/Input";
import Card from "../../UI/card/Card";
import classes from "./Login.module.css";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { signIn, getSession } from "next-auth/react";
import { useRouter } from "next/router";
const Login = () => {
const route = useRouter();
const {
register,
handleSubmit,
formState: { errors }
} = useForm();
const submittedFormHandler = async (userInputs) => {
const result = await signIn("credentials", {
redirect: false,
email: userInputs.userNameEmail,
userName: userInputs.userNameEmail,
password: userInputs.password
}); /* result will always resolve */
if (!result.error) {
route.replace("/");
}
};
return (
<>
<Card className={classes.login}>
<form onSubmit={handleSubmit(submittedFormHandler)}>
<Input
htmlFor="userNameEmail"
id="userNameEmail"
label="UserName or Email"
input={{
type: "text",
...register("userNameEmail", { required: true})
}}
></Input>
<span className={classes.spanning}>
{errors.userName &&
"Enter userName or Email at least four characters"}
</span>
<Input
htmlFor="password"
id="password"
label="Enter Password"
input={{
type: "password",
...register("password", { required: true, minLength: 8 })
}}
></Input>
<span className={classes.spanning}>
{errors.password && "password should be at least 8 characters"}
</span>
<div className={classes.password}>
<Button type="submit">Submit</Button>
<Link href="/ForgotPassword">Forgot Password ?</Link>
</div>
<Link href="/NewUser" className={classes.link}>
Create Account New User
</Link>
</form>
</Card>
</>
);
};
export async function getServerSideProps(context) {
const session = await getSession({
req: context.req
}); //returns session obj or null
if (session) {
return {
redirect: {
destination: "/",
permanent: false
}
};
}
return {
props: { session }
};
}
export default Login;
what could be the problem? please assist
I faced the same problem, but i was using mysql as database and i didn't use the auth file middleware that suggest next-auth to handle the providers, instead i created a separated file to handle sequelize (in your case will be the orm with the database you're using).
I fixed it adding dialectModule to the propertys of the class Sequelize
const db = new Sequelize(`${process.env.DB_URI}`, {
database: process.env.DB_NAME,
logging: false,
dialect: "mssql",
dialectModule: require("mysql2"),
});
I also have this problem. They said on the doc that make sure you define NEXTAUTH_URL variable correctly. If you use Vercel to host, then the content of the variable should be only the url without quote. For example, https:project.vercel.app.
If not solved, try changing the [...nextauth].ts file to a more simple version. I got this error when I tried doing things with database in the callback (mongodb in my case) like this
async jwt({ token }) {
let role:Role = 'user'
if (!token.email) throw Error('no email provided with token')
let user = await getUser(token.email)
if (user?.isAdmin) role = 'admin'
return {...token, role}
}
After removing this, my problem is solved. In your case, you could try removing anything that deals with the database.
After I got that working, I added this callback function instead
async jwt({ token }) {
let role:Role = 'user'
if (!token.email) throw Error('no email provided with token')
const client = await clientPromise
const collection = client.db().collection('users')
const user = await collection.findOne({email:token.email})
if (user?.isAdmin) role = 'admin'
return {...token, role}
}
The only difference is that the first one use mongoose, and the second one doesn't. The approach of the second one is taken from https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
Disclaimer: I don't know why it worked.

Making post request with React

In my React application, I have a form with only username and password (later I will add "confirm password" as well), when submitting a request with JSON should be send that contains the email and password in its body.
Password can only be accepted after few checks and if it passes all of those conditions then it will be accepted.
render() {
return (
<form className="demoForm" onSubmit={this.handleUserInput} >
.
.
.
.
<button type="submit" className="btn btn-primary" disabled={!this.state.formValid}>Sign U p</button>
</form>
);
}
handleUserInput = (e) => {
const name = e.target.name;
const value = e.target.value;
this.setState({[name]: value}, () => { this.validateField(name, value) });
axios.post('****', {
value
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
}
I am using axios like above, my problem is that I dont know what should be instead of this ****, I am using local host. Is this a good way to do this?
You should add the address you are posting your call to. (i.e. /api/validate_user)
Just on a side note, try separating your actions.
onChangeHandler(e) {
e.preventDefault()
const { value, id} = e.target
this.setState({
[id]: value
})
}
to update the state and then to submit
onSubmitHandler(e) {
var self = this;
e.preventDefault()
const { userName, password } = this.state;
// Do validation of password
axios.post('/api/validateUser', {
user: userName,
password: password
}).then(e => {
if(e.success){
console.log("success")
}
else if(e.error) {
console.log("error logging in")
}
})
}

Get data when login by facebook?

I have this function to log in by facebook method in firebase with React Native :
async handleFacebookButton() {
const navigation = this.props.navigation;
const { type, token } = await Facebook.logInWithReadPermissionsAsync(FACEBOOK_APP_ID, {
permissions: ['public_profile', 'email']
});
if (type === 'success') {
const credential = firebase.auth.FacebookAuthProvider.credential(token);
navigation.navigate("Profile");
auth.signInWithCredential(credential).catch(error => {
this.setState({ errorMessage: error.message });
alert('please check your email or password');
});
}
}
And I need to get the user data when login, like username, phone, email.
how can I get the data?
You need a .then() in your auth.signInWithCredential(). Then you'd have something like:
auth.signInWithCredential().then(user => {
// user is the signed in user, for which you can get details
})

Categories