I’m trying to understand why user authorization works on localhost frontend, but doesn’t work on 127.0.0.1.
What I have done?
- Changed CORS origins (didn’t work);
- Use credentials, samePage, no-cors (didn’t work);
- ExtractJwt.fromAuthHeaderWithScheme(‘Bearer’) (didn’t work);
My dependencies:
- “cookie-parser”: “^1.4.6”,
- “cors”: “^2.8.5”,
- “express”: “^4.19.2”,
- “jsonwebtoken”: “^9.0.2”,
- “passport”: “^0.7.0”,
- “passport-jwt”: “^4.0.1”,
- “passport-local”: “^1.0.0”
Frontend
api.js:
const API_HOST = 'http://localhost:3000';
class ApiService {
async request(path, method = 'GET', body = null) {
console.log('Request', { path, method, body });
const url = [API_HOST, path].join(path[0] === '/' ? '' : '/');
const isJson = body && body instanceof Object ? JSON.stringify(body) : body;
const headers = new Headers();
if (isJson) {
headers.set('Content-Type', 'application/json');
}
const res = await fetch(url, {
method: method,
credentials: 'include',
headers: headers,
body: isJson ? JSON.stringify(body) : body
});
let data = await res.text();
try {
data = JSON.parse(data);
} catch (err) {}
if (res.status < 200 || res.status >= 300) {
throw new Error(data instanceof Object ? data.err : data);
}
return data;
}
}
class UserApiService extends ApiService {
login(username, password) {
return this.request('/login', 'POST', { username, password });
}
logout() {
return this.request('/logout', 'POST');
}
me() {
return this.request('/me', 'GET');
}
}
login.html:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Page</title>
<script src="./api.js"></script>
</head>
<body>
<form action="#" id="loginForm">
<input type="text" name="username">
<input type="password" name="password">
<button type="submit">Login</button>
</form>
<script>
const userApi = new UserApiService();
const formElement = document.getElementById('loginForm');
formElement.addEventListener('submit', async (event) => {
event.preventDefault();
const username = event.target.username.value;
const password = event.target.password.value;
const user = await userApi.login(username, password);
location.pathname = '/admin.html';
});
</script>
</body>
</html>
admin.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Page</title>
<script src="./api.js"></script>
</head>
<body>
<h1 id="title"></h1>
<button id="logoutButton">Logout</button>
<script>
const userApi = new UserApiService();
document.getElementById('logoutButton').addEventListener('click', async () => {
await userApi.logout();
location.pathname = '/login.html';
});
userApi.me()
.then(user => {
const titleElement = document.getElementById('title');
titleElement.innerText = `Welcome, ${user.username}!`
})
.catch(err => {
location.pathname = '/login.html';
throw err;
});
</script>
</body>
</html>
Backend
index.js:
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
const passport = require('passport');
const { Strategy: LocalStrategy } = require('passport-local');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const config = require('./config');
//! Replace 'config.devTest' parts with real logic
/* ----------------[ Custom Errors ] ----------------*/
class APIError extends Error {
constructor(status, message = '') {
super(message);
this.status = status;
}
}
/* ----------------[ Instances and Constants ] ----------------*/
const app = express();
const PORT = process.env.PORT || 3000;
const requireAuth = passport.authenticate('jwt', { session: false });
/* ----------------[ Passport Initialization ] ----------------*/
passport.use(
'local',
new LocalStrategy(
(username, password, done) => {
const { user } = config.devTest;
if (
username !== user.username ||
password !== user.password
) {
done(new APIError(401, 'Invalid username or password'));
}
// Generates new JWT token
const token = jwt.sign({ id: user.id }, config.jwtSecret, { expiresIn: '1h' });
done(null, token);
}
)
);
passport.use(
'jwt',
new JwtStrategy(
{
secretOrKey: config.jwtSecret,
jwtFromRequest: ExtractJwt.fromExtractors([
// Extract JWT from auth header
ExtractJwt.fromAuthHeaderAsBearerToken(),
// Extract JWT from cookies
(req) => req.cookies[config.cookieSessionKey]
]),
},
(payload, done) => {
const { id: userId } = payload;
if (userId && userId === config.devTest.user.id) {
return done(null, config.devTest.user);
} else {
return done(new APIError(401, 'Validation Failed'), false);
}
}
)
);
// Used to serialize user (from object to id)
passport.serializeUser((user, done) => {
done(null, user.id)
});
// Used to deserialize user (from id to object)
passport.deserializeUser((id, done) => {
const user = { id, username: 'User' }; // Replace this part with real user find logic
done(null, user);
});
/* ----------------[ App Initialization ] ----------------*/
app.use(cors({
// Similar to the '*' wildcard, but bypasses cors restrictions
origin: (origin, callback) => callback(null, true),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
app.use(cookieParser());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(passport.initialize());
/* ----------------[ App Routes ] ----------------*/
app.post('/login', passport.authenticate('local', { session: false }), async (req, res) => {
const token = req.user;
// Saves and sends JWT token to the user
res
.cookie(config.cookieSessionKey, token, { httpOnly: true, secure: false })
.json({ token });
});
app.post('/logout', requireAuth, async (req, res) => {
res
.clearCookie(config.cookieSessionKey)
.end();
});
app.get('/me', requireAuth, async (req, res) => {
res.json(req.user);
});
// Global error handler
app.use((err, req, res, next) => {
if (err instanceof APIError) {
res.status(err.status).json({ err: err.message });
return;
}
res.sendStatus(500);
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
New contributor
Daniel is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.