This is another simple guide that I am putting together for my future self. As I make my latest app from the ground up I want to document my experiences. This is not meant to be a thorough guide – it’s just a list of steps needed to accomplish the goal.
yarn add jsonwebtoken
yarn add passport passport-jwt passport-local
Perform this in the server folder. This package contains the platform for various authentication methods that can be used.
passport-jwt will handle the protected route secret if the user is authenticated from a provider. “local” handles the email/password scenario.
const passport = require("passport");
const LocalStrategy = require("passport-local");
const passportJWT = require("passport-jwt");
Create the corresponding variables in your server file (in my case, “/server.js”).
var apiRouter = require('./server/routes/api');
router.use('/api', apiRouter);
app.use(express.static("client/build"));
Put the api routes before the the declaration of the client-side public files (more information on the latter here).
const passport = require("passport");
const LocalStrategy = require("passport-local");
const passportJWT = require("passport-jwt");
JWTStrategy = passportJWT.Strategy;
const app = express();
app.use(passport.initialize());
Initializing the JWT strategy and passport instance.
The jwt token is split into three parts – header, payload and signature. This is essentially an object encoded as a string. The signature verifies that your token is legitimate.
const user = {
id: "1",
email: "example@example.com",
password : "password"
}
passport.use(new JWTStrategy({
jwtFromRequest: passportJWT.ExtractJwt.fromAuthHeaderAsBearerToken(), // where are we getting JWT token from
secretOrKey:"jwt_secret" // make this more complex
}, (jwt_payload, done) => { // payload coming back
if (user.id === jwt_payload.user._id){
return done(null, user)
} else {
return done(null, false, {
message: "the user was not found"
})
}
}))
In this step we look to see if the user in the incoming request matches a user. For now we will compare it to the temporary user we created earlier. Note that the jwt_secret is defined here – in my prod script this is moved to an environment variable (see here).
passport.use(new LocalStrategy({
usernameField:"email"
}, (email, password, done) => { // callback for authentication
if (email === user.email && password === user.password) {
return done(null, user);
} else {
return done(mull, false)
}
}))
Here we are handling the email/password scenario. This is very similar to the previous code block – instead of extracting the JWT token from the authentication header we are comparing the username and password.
<ThemeProvider theme={theme}>
<UserProfileProvider>
<BrowserRouter>
<Header value={value} setValue={setValue} selectedIndex={selectedIndex} setSelectedIndex={setSelectedIndex} />
<Switch>
<Route exact path="/" component = {Dashboard} />
<Route exact path="/login" component = {SignIn} />
Use browser router to handle client-side routing.
const server = process.env.REACT_APP_API_SERVER;
export const login = (email, password) => {
return fetch(`${server}/api/login`, {
method: "POST",
body: JSON.stringify({
email,
password,
}),
headers: {
"Content-Type": "application/json",
},
}).then((res) => {
if (res.status === 200) {
return res.json().then((data) => {
localStorage.setItem('token', data.token); // for testing only
return data.token
});
} else {
const error = new Error(res.error);
throw error;
}
});
};
export const getSecret = () => {};
I created this helper utility “api” to handle the call to the backend server. It will be used in Login component.
const history = useHistory();
const onSubmit = async (event) => {
event.preventDefault();
try {
const token = await login(event.target.email.value, event.target.password.value)
console.log(token)
history.push("/");
} catch (error) {
console.error(error);
alert("Error logging in please try again");
}
};
From the Login component. Note that the onSubmit is attached to form in the html. If successful login then route to the root of the app.
app.post("/login", (req, res, next) => {
passport.authenticate("local", (err, user) => {
if(err){
return next(err)
}
if(!user){
return res.send("Wrong email or password")
}
req.login(user, () => {
const body = {_id: user.id, email: user.email }
const token = jwt.sign({user: body}, "jwt_secret")
return res.json({token})
})
})(req, res, next)
})
We now need to account for the api call back to the express server in the api router file. Note that I left the jwt_secret string in here for reference only – it comes from an environment variable in the real code.