Compare commits

..

5 Commits

Author SHA1 Message Date
Kayne Ruse 4ec55bed10 Bumped version number 2021-07-28 15:43:47 +01:00
Kayne Ruse b51d22f1a1 SQUASH: I think I got it working 2021-07-29 00:36:20 +10:00
Kayne Ruse 6b3db67a29 Fixed whitespace 2021-07-28 23:19:08 +10:00
Kayne Ruse 81da8ca422 Working on password recovery 2021-07-28 23:02:04 +10:00
Kayne Ruse 72b3babfd8 Reworking JWT authentication 2021-07-28 21:36:04 +10:00
22 changed files with 323 additions and 86 deletions
+1
View File
@@ -1,5 +1,6 @@
WEB_PROTOCOL=http
WEB_ADDRESS=localhost
WEB_RESET_ADDRESS=localhost/reset
WEB_PORT=3200
DB_HOSTNAME=database
+32 -1
View File
@@ -19,9 +19,11 @@ Content-Type: application/json
"password": "helloworld"
}
//DOCS: Used for validating the email address above
GET /auth/validation?username=example&token=12345678
//DOCS: Login after validation
POST /auth/login
Content-Type: application/json
@@ -37,7 +39,8 @@ Content-Type: application/json
"refreshToken": "fghij"
}
//Replace an expired authToken pair with these values
//DOCS: Replace an expired authToken pair with these values
POST /auth/token
Content-Type: application/json
@@ -45,6 +48,7 @@ Content-Type: application/json
"token": "refreshToken"
}
//DOCS: After this is called, the refresh route will no longer work
DELETE /auth/logout
Authorization: Bearer accessToken
@@ -53,6 +57,7 @@ Authorization: Bearer accessToken
"token": "refreshToken"
}
//DOCS: Retreives the private account data, results vary
GET /auth/account
Authorization: Bearer accessToken
@@ -63,11 +68,13 @@ Authorization: Bearer accessToken
"refreshToken": "fghij"
}
//DOCS: Update account data, input varies, but is always JSON
PATCH /auth/account
Content-Type: application/json
Authorization: Bearer accessToken
//DOCS: Sets the timer, account will be deleted after 2 days
DELETE /auth/account
Authorization: Bearer accessToken
@@ -76,4 +83,28 @@ Content-Type: application/json
{
"password": "helloworld"
}
//DOCS: Send the link to recover a forgotten password
POST /auth/recover
Content-Type: application/json
{
"email": "kayneruse@gmail.com"
}
//DOCS: Redirect the link to recover a password to the front-end
GET /auth/reset?token=<token>
//Result
301 -> ${WEB_RESET_ADDRESS}?email=<email>&token=<token>
//DOCS: Resets a password for the given email, correct token is required
PATCH /auth/reset?email=<email>&token=<token>
{
"password": "password"
}
```
+2
View File
@@ -30,6 +30,7 @@ const question = (prompt, def = null) => {
//project configuration
const appName = await question('App Name', 'auth');
const appWebAddress = await question('Web Addr', `${appName}.example.com`);
const resetAddress = await question('Reset Addr', `example.com/reset`);
const appPort = await question('App Port', '3200');
const appDBUser = await question('DB User', appName);
@@ -69,6 +70,7 @@ services:
environment:
- WEB_PROTOCOL=https
- WEB_ADDRESS=${appWebAddress}
- WEB_RESET_ADDRESS=${resetAddress}
- WEB_PORT=${appPort}
- DB_HOSTNAME=database
- DB_DATABASE=${appName}
+36 -36
View File
@@ -1,36 +1,36 @@
{
"name": "auth-server",
"version": "1.3.2",
"description": "An API centric auth server. Uses Sequelize and mariaDB by default.",
"main": "server/server.js",
"scripts": {
"start": "node server/server.js",
"dev": "npm run watch:server",
"watch:server": "nodemon . --ext js,jsx,json --ignore 'node_modules/*'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/krgamestudios/auth-server.git"
},
"author": "Kayne Ruse",
"license": "ISC",
"bugs": {
"url": "https://github.com/krgamestudios/auth-server/issues"
},
"homepage": "https://github.com/krgamestudios/auth-server#readme",
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"dotenv": "^8.6.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"mariadb": "^2.5.4",
"node-cron": "^2.0.3",
"nodemailer": "^6.6.3",
"sequelize": "^6.6.5"
},
"devDependencies": {
"nodemon": "^2.0.12"
}
}
{
"name": "auth-server",
"version": "1.4.1",
"description": "An API centric auth server. Uses Sequelize and mariaDB by default.",
"main": "server/server.js",
"scripts": {
"start": "node server/server.js",
"dev": "npm run watch:server",
"watch:server": "nodemon . --ext js,jsx,json --ignore 'node_modules/*'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/krgamestudios/auth-server.git"
},
"author": "Kayne Ruse",
"license": "ISC",
"bugs": {
"url": "https://github.com/krgamestudios/auth-server/issues"
},
"homepage": "https://github.com/krgamestudios/auth-server#readme",
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"dotenv": "^8.6.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"mariadb": "^2.5.4",
"node-cron": "^2.0.3",
"nodemailer": "^6.6.3",
"sequelize": "^6.6.5"
},
"devDependencies": {
"nodemon": "^2.0.12"
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ const route = async (req, res) => {
//forcibly logout
tokens.destroy({
where: {
username: req.body.username || ''
email: req.body.email || ''
}
});
+1 -1
View File
@@ -12,7 +12,7 @@ router.use(tokenAuth);
router.use(async (req, res, next) => {
const record = await accounts.findOne({
where: {
username: req.user.username || ''
email: req.user.email || ''
}
});
+6 -1
View File
@@ -14,6 +14,11 @@ router.post('/login', require('./login'));
//refresh token
router.post('/token', require('./token'));
//password recover and reset
router.post('/recover', require('./password-recover'));
router.get('/reset', require('./password-redirect'));
router.patch('/reset', require('./password-reset'));
//middleware
router.use(tokenAuth);
@@ -23,7 +28,7 @@ router.delete('/logout', require('./logout'));
router.use(async (req, res, next) => {
const record = await accounts.findOne({
where: {
username: req.user.username || ''
email: req.user.email || ''
}
});
+3 -3
View File
@@ -3,7 +3,7 @@ const utils = require('util');
const bcrypt = require('bcryptjs');
const { accounts } = require('../database/models');
const generate = require('../utilities/token-generate');
const tokenGenerate = require('../utilities/token-generate');
//utilities
const validateEmail = require('../utilities/validate-email');
@@ -48,10 +48,10 @@ const route = async (req, res) => {
}
//generate the JWT
const tokens = generate(account.index, account.username, account.type, account.admin, account.mod);
const token = tokenGenerate(account.index, account.email, account.username, account.type, account.admin, account.mod);
//finally
res.status(200).json(tokens);
res.status(200).json(token);
};
const validateDetails = async (body) => {
+2 -2
View File
@@ -1,8 +1,8 @@
const destroy = require('../utilities/token-destroy');
const tokenDestroy = require('../utilities/token-destroy');
//auth/logout
const route = (req, res) => {
destroy(req.body.token);
tokenDestroy(req.body.token);
return res.status(200).end();
};
+106
View File
@@ -0,0 +1,106 @@
//libraries
const nodemailer = require('nodemailer');
const { accounts, recovery } = require('../database/models');
//utilities
const uuid = require('../utilities/uuid');
const validateEmail = require('../utilities/validate-email');
//auth/recover
const route = async (req, res) => {
//validate details
const validateErr = await validateDetails(req.body);
if (validateErr) {
return res.status(401).end(validateErr);
}
//recovery token
const token = uuid(32);
//send the recovery email
const emailErr = await sendRecoveryEmail(req.body.email, token);
if (emailErr) {
return res.status(500).send(emailErr);
}
//save the token
await recovery.upsert({
email: req.body.email,
token: token
});
//finally
res.status(200).send("Recovery email sent!");
return null;
};
const validateDetails = async (body) => {
//basic formatting
if (!validateEmail(body.email)) {
return 'Invalid email';
}
//check for existing email
const emailRecord = await accounts.findOne({
where: {
email: body.email
}
});
if (!emailRecord) {
return 'Invalid email';
}
//OK
return null;
};
const sendRecoveryEmail = async (email, token) => {
const addr = `${process.env.WEB_PROTOCOL}://${process.env.WEB_ADDRESS}/auth/reset?token=${token}`;
const msg = `Hello,
Please visit the following link to reset your password: ${addr}
If you did not request a password reset, you can safely ignore this message.
`;
let transporter, info;
//what exactly is a transport?
try {
transporter = nodemailer.createTransport({
host: process.env.MAIL_SMTP,
port: 465,
secure: true,
auth: {
user: process.env.MAIL_USERNAME,
pass: process.env.MAIL_PASSWORD
},
});
}
catch(e) {
return `failed to create a mail transport: ${e}`;
}
// send mail with defined transport object
try {
info = await transporter.sendMail({
from: `recovery@${process.env.WEB_ADDRESS}`, //WARNING: google overwrites this
to: email,
subject: 'Password Recovery',
text: msg
});
}
catch(e) {
return `failed to send validation mail: ${e}`;
}
if (info.accepted[0] != email) {
return 'recovery email failed to send';
}
return null;
};
module.exports = route;
+21
View File
@@ -0,0 +1,21 @@
const { accounts, recovery } = require('../database/models');
//auth/reset
const route = async (req, res) => {
//verify the recovery record exists
const record = await recovery.findOne({
where: {
token: req.query.token
}
});
if (!record) {
return res.status(401).end('Failed to recover a password');
}
//redirect to the front-end
res.redirect(`${process.env.WEB_PROTOCOL}://${process.env.WEB_RESET_ADDRESS}?email=${record.email}&token=${record.token}`);
return null;
};
module.exports = route;
+62
View File
@@ -0,0 +1,62 @@
//libraries
const bcrypt = require('bcryptjs');
const { accounts, recovery } = require('../database/models');
//auth/reset
const route = async (req, res) => {
//validate the given details
const validateErr = await validateDetails(req.query, req.body);
if (validateErr) {
return res.status(401).send(validateErr);
}
//generate the password hash
const hash = await bcrypt.hash(req.body.password, await bcrypt.genSalt(11));
//update the account data
await accounts.update({
hash: hash
}, {
where: {
email: req.query.email
}
})
//delete from the recovery table
await recovery.destroy({
where: {
email: req.query.email
}
});
res.status(200).end();
return null;
};
const validateDetails = async (query, body) => {
//verify the recovery record exists
const record = await recovery.findOne({
where: {
email: query.email,
token: query.token
}
});
if (!record) {
return 'Failed to recover a password';
}
//validate password
if (!body.password) {
return 'Missing password';
}
if (body.password.length < 8) {
return 'Password too short';
}
return null;
};
module.exports = route;
-1
View File
@@ -6,7 +6,6 @@ const Op = Sequelize.Op;
const { accounts, pendingSignups } = require('../database/models');
//utilities
const uuid = require('../utilities/uuid');
const validateEmail = require('../utilities/validate-email');
+3 -3
View File
@@ -1,16 +1,16 @@
const jwt = require('jsonwebtoken');
const refresh = require('../utilities/token-refresh');
const tokenRefresh = require('../utilities/token-refresh');
//auth/token
module.exports = async (req, res) => {
const refreshToken = req.body.token;
return refresh(refreshToken, (err, tokens) => {
return tokenRefresh(refreshToken, (err, token) => {
if (err) {
return res.status(err).end();
}
return res.status(200).send(tokens);
return res.status(200).send(token);
});
};
+2 -1
View File
@@ -1,5 +1,6 @@
module.exports = {
tokens: require('./tokens'),
accounts: require('./accounts'),
pendingSignups: require('./pending-signups')
pendingSignups: require('./pending-signups'),
recovery: require('./recovery')
};
+6
View File
@@ -0,0 +1,6 @@
const sequelize = require('..');
module.exports = sequelize.define('recovery', {
token: 'varchar(320)',
email: 'varchar(320)'
});
+1 -1
View File
@@ -3,5 +3,5 @@ const sequelize = require('..');
module.exports = sequelize.define('tokens', {
token: 'varchar(320)',
username: 'varchar(320)' //TODO: why username?
email: 'varchar(320)'
});
+32 -32
View File
@@ -1,32 +1,32 @@
//environment variables
require('dotenv').config();
//create the server
const express = require('express');
const app = express();
const server = require('http').Server(app);
const cors = require('cors');
//config
app.use(express.json());
app.use(cors());
//database connection
const database = require('./database');
//access the admin
app.use('/admin', require('./admin'));
//access the auth
app.use('/auth', require('./auth'));
//error on access
app.get('*', (req, res) => {
res.redirect('https://github.com/krgamestudios/auth-server');
});
//startup
server.listen(process.env.WEB_PORT || 3200, async (err) => {
await database.sync();
console.log(`listening to localhost:${process.env.WEB_PORT || 3200}`);
});
//environment variables
require('dotenv').config();
//create the server
const express = require('express');
const app = express();
const server = require('http').Server(app);
const cors = require('cors');
//config
app.use(express.json());
app.use(cors());
//database connection
const database = require('./database');
//access the admin
app.use('/admin', require('./admin'));
//access the auth
app.use('/auth', require('./auth'));
//error on access
app.get('*', (req, res) => {
res.redirect('https://github.com/krgamestudios/auth-server');
});
//startup
server.listen(process.env.WEB_PORT || 3200, async (err) => {
await database.sync();
console.log(`listening to localhost:${process.env.WEB_PORT || 3200}`);
});
+3 -2
View File
@@ -2,9 +2,10 @@ const jwt = require('jsonwebtoken');
const { tokens } = require('../database/models');
//generates a JWT token based on the given arguments
module.exports = (index, username, type, admin, mod) => {
module.exports = (index, email, username, type, admin, mod) => {
const content = {
index,
email,
username,
type,
admin,
@@ -14,7 +15,7 @@ module.exports = (index, username, type, admin, mod) => {
const accessToken = jwt.sign(content, process.env.SECRET_ACCESS, { expiresIn: '10m' });
const refreshToken = jwt.sign(content, process.env.SECRET_REFRESH, { expiresIn: '30d' });
tokens.create({ token: refreshToken, username: username });
tokens.create({ token: refreshToken, email: email });
return { accessToken, refreshToken };
};
+1 -1
View File
@@ -24,7 +24,7 @@ module.exports = (token, callback) => {
return callback(403);
}
const result = generate(user.index, user.username, user.type, user.admin, user.mod);
const result = generate(user.index, user.email, user.username, user.type, user.admin, user.mod);
destroy(token);
+1
View File
@@ -0,0 +1 @@
ALTER TABLE `accounts` CHANGE `id` `index` INT( 11 ) NOT NULL AUTO_INCREMENT;
+1
View File
@@ -0,0 +1 @@
DROP TABLE tokens;