├── .gitignore ├── .DS_Store ├── config ├── config.json └── passport.js ├── utils └── auth.js ├── README.md ├── models ├── user.js └── index.js ├── oauthModel.js ├── package.json ├── app.js ├── migrations └── 20250718102426-create-user.js ├── LICENSE └── routes └── auth.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .git/ 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/didinj/secure-node-express-postgresql-passport/HEAD/.DS_Store -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "djamware", 4 | "password": "dj@mw@r3", 5 | "database": "secure_api_db", 6 | "host": "127.0.0.1", 7 | "dialect": "postgres" 8 | } 9 | } -------------------------------------------------------------------------------- /utils/auth.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcrypt"; 2 | const SALT_ROUNDS = 10; 3 | export const hashPassword = pw => bcrypt.hash(pw, SALT_ROUNDS); 4 | export const comparePassword = (pw, hash) => bcrypt.compare(pw, hash); 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secure Node.js, Express, and PostgreSQL REST API with JWT and Optional OAuth2 (2025 Edition) 2 | 3 | Read the full tutorial [here](https://www.djamware.com/post/5bf94d9a80aca747f4b9ce9f/secure-nodejs-express-and-postgresql-rest-api-with-jwt-and-optional-oauth2-2025-edition). 4 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | export default (sequelize, DataTypes) => { 2 | const User = sequelize.define("User", { 3 | username: DataTypes.STRING, 4 | password: DataTypes.STRING 5 | }); 6 | 7 | User.associate = function(models) { 8 | // associations can be defined here 9 | }; 10 | 11 | return User; 12 | }; 13 | -------------------------------------------------------------------------------- /oauthModel.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getAccessToken: async function(token) { 3 | // Implement token retrieval logic 4 | return { 5 | accessToken: token, 6 | client: {}, 7 | user: {}, 8 | accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000) 9 | }; 10 | }, 11 | 12 | getClient: async function(clientId, clientSecret) { 13 | return { id: clientId, grants: ["password"] }; 14 | }, 15 | 16 | saveToken: async function(token, client, user) { 17 | return { ...token, client, user }; 18 | }, 19 | 20 | getUser: async function(username, password) { 21 | // Validate and return a user 22 | return { id: 1, username }; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | import passport from "passport"; 2 | import { Strategy as JwtStrategy, ExtractJwt } from "passport-jwt"; 3 | import db from "../models/index.js"; 4 | const { User } = db; 5 | import dotenv from "dotenv"; 6 | dotenv.config(); 7 | 8 | passport.use( 9 | new JwtStrategy( 10 | { 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | secretOrKey: process.env.JWT_SECRET 13 | }, 14 | async (payload, done) => { 15 | try { 16 | const user = await User.findByPk(payload.id); 17 | return done(null, user || false); 18 | } catch (err) { 19 | return done(err, false); 20 | } 21 | } 22 | ) 23 | ); 24 | 25 | export default passport; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-node-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "type": "module", 13 | "dependencies": { 14 | "bcrypt": "^6.0.0", 15 | "dotenv": "^17.2.0", 16 | "express": "^5.1.0", 17 | "express-oauth-server": "^2.0.0", 18 | "jsonwebtoken": "^9.0.2", 19 | "passport": "^0.7.0", 20 | "passport-jwt": "^4.0.1", 21 | "pg": "^8.16.3", 22 | "pg-hstore": "^2.3.4", 23 | "sequelize": "^6.37.7" 24 | }, 25 | "devDependencies": { 26 | "nodemon": "^3.1.10", 27 | "sequelize-cli": "^6.6.3" 28 | } 29 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import passport from "./config/passport.js"; 3 | import authRoutes from "./routes/auth.js"; 4 | import dotenv from "dotenv"; 5 | import OAuthServer from "express-oauth-server"; 6 | import oauthModel from "./oauthModel.js"; 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | app.use(express.json()); 11 | app.use(passport.initialize()); 12 | app.use("/api/auth", authRoutes); 13 | 14 | const port = process.env.PORT || 3000; 15 | app.listen(port, () => 16 | console.log(`Server running on http://localhost:${port}`) 17 | ); 18 | app.oauth = new OAuthServer({ model: oauthModel }); 19 | 20 | app.post("/oauth/token", app.oauth.token()); 21 | app.get("/secure", app.oauth.authenticate(), (req, res) => 22 | res.send("Secure Data") 23 | ); 24 | -------------------------------------------------------------------------------- /migrations/20250718102426-create-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** @type {import('sequelize-cli').Migration} */ 3 | module.exports = { 4 | async up(queryInterface, Sequelize) { 5 | await queryInterface.createTable('Users', { 6 | id: { 7 | allowNull: false, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | type: Sequelize.INTEGER 11 | }, 12 | username: { 13 | type: Sequelize.STRING 14 | }, 15 | password: { 16 | type: Sequelize.STRING 17 | }, 18 | createdAt: { 19 | allowNull: false, 20 | type: Sequelize.DATE 21 | }, 22 | updatedAt: { 23 | allowNull: false, 24 | type: Sequelize.DATE 25 | } 26 | }); 27 | }, 28 | async down(queryInterface, Sequelize) { 29 | await queryInterface.dropTable('Users'); 30 | } 31 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Didin Jamaludin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | import dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | 11 | const basename = path.basename(__filename); 12 | const env = process.env.NODE_ENV || 'development'; 13 | const config = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/config.json')))[env]; 14 | 15 | const sequelize = new Sequelize(config.database, config.username, config.password, config); 16 | 17 | const db = {}; 18 | 19 | const modelFiles = fs 20 | .readdirSync(__dirname) 21 | .filter( 22 | file => file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' 23 | ); 24 | 25 | for (const file of modelFiles) { 26 | const model = await import(path.join(__dirname, file)); 27 | const definedModel = model.default(sequelize, Sequelize.DataTypes); 28 | db[definedModel.name] = definedModel; 29 | } 30 | 31 | Object.keys(db).forEach(modelName => { 32 | if (db[modelName].associate) { 33 | db[modelName].associate(db); 34 | } 35 | }); 36 | 37 | db.sequelize = sequelize; 38 | db.Sequelize = Sequelize; 39 | 40 | export default db; -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import jwt from "jsonwebtoken"; 3 | import passport from "../config/passport.js"; 4 | import db from "../models/index.js"; 5 | const { User } = db; 6 | import { hashPassword, comparePassword } from "../utils/auth.js"; 7 | import dotenv from "dotenv"; 8 | dotenv.config(); 9 | 10 | const router = express.Router(); 11 | 12 | router.post("/signup", async (req, res) => { 13 | const { username, password } = req.body; 14 | const hashed = await hashPassword(password); 15 | const user = await User.create({ username, password: hashed }); 16 | res.json({ id: user.id, username: user.username }); 17 | }); 18 | 19 | router.post("/signin", async (req, res) => { 20 | const { username, password } = req.body; 21 | const user = await User.findOne({ where: { username } }); 22 | if (!user || !await comparePassword(password, user.password)) { 23 | return res.status(401).json({ message: "Invalid credentials" }); 24 | } 25 | const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { 26 | expiresIn: "1h" 27 | }); 28 | res.json({ token }); 29 | }); 30 | 31 | router.get( 32 | "/profile", 33 | passport.authenticate("jwt", { session: false }), 34 | (req, res) => { 35 | res.json({ id: req.user.id, username: req.user.username }); 36 | } 37 | ); 38 | 39 | export default router; 40 | --------------------------------------------------------------------------------