wip: move all authentication to passport, including JWT and add new AccessToken and RefreshToken (which are not issued yet)
This commit is contained in:
parent
fc99d8a03b
commit
254b262a72
40
api/package-lock.json
generated
40
api/package-lock.json
generated
|
@ -6243,6 +6243,16 @@
|
|||
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
|
||||
},
|
||||
"oauth2orize": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth2orize/-/oauth2orize-1.11.0.tgz",
|
||||
"integrity": "sha1-eTzvJR1F696sMq5AqLaBT6qx1IM=",
|
||||
"requires": {
|
||||
"debug": "2.x.x",
|
||||
"uid2": "0.0.x",
|
||||
"utils-merge": "1.x.x"
|
||||
}
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
@ -6471,6 +6481,31 @@
|
|||
"pause": "0.0.1"
|
||||
}
|
||||
},
|
||||
"passport-custom": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz",
|
||||
"integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==",
|
||||
"requires": {
|
||||
"passport-strategy": "1.x.x"
|
||||
}
|
||||
},
|
||||
"passport-http-bearer": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/passport-http-bearer/-/passport-http-bearer-1.0.1.tgz",
|
||||
"integrity": "sha1-FHRp6jZp4qhMYWfvmdu3fh8AmKg=",
|
||||
"requires": {
|
||||
"passport-strategy": "1.x.x"
|
||||
}
|
||||
},
|
||||
"passport-jwt": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz",
|
||||
"integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==",
|
||||
"requires": {
|
||||
"jsonwebtoken": "^8.2.0",
|
||||
"passport-strategy": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"passport-local": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
|
||||
|
@ -8566,6 +8601,11 @@
|
|||
"random-bytes": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"uid2": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
|
||||
"integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I="
|
||||
},
|
||||
"undefsafe": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz",
|
||||
|
|
|
@ -41,8 +41,12 @@
|
|||
"mongoose-unique-validator": "2.0.3",
|
||||
"morgan": "1.10.0",
|
||||
"nodemailer": "^6.4.18",
|
||||
"oauth2orize": "^1.11.0",
|
||||
"passport": "0.4.1",
|
||||
"passport-local": "1.0.0",
|
||||
"passport-custom": "^1.1.1",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"request": "2.88.2",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"slug": "^3.5.2",
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
const passport = require('passport');
|
||||
const LocalStrategy = require('passport-local').Strategy;
|
||||
const mongoose = require('mongoose');
|
||||
const User = mongoose.model('User');
|
||||
const { Strategy: LocalStrategy } = require('passport-local');
|
||||
const { Strategy: BearerStrategy } = require('passport-http-bearer');
|
||||
const { Strategy: JwtStrategy } = require('passport-jwt');
|
||||
const { Strategy: CustomStrategy } = require('passport-custom');
|
||||
|
||||
const { User, AccessToken, RefreshToken } = require('../models');
|
||||
|
||||
const secret = require('../config').secret;
|
||||
|
||||
passport.use(
|
||||
'usernameAndPassword',
|
||||
new LocalStrategy(
|
||||
{
|
||||
usernameField: 'user[email]',
|
||||
passwordField: 'user[password]',
|
||||
session: false,
|
||||
},
|
||||
async function (email, password, done) {
|
||||
try {
|
||||
|
@ -16,10 +23,6 @@ passport.use(
|
|||
return done(null, false, { errors: { 'email or password': 'is invalid' } });
|
||||
}
|
||||
|
||||
if (user.needsEmailValidation) {
|
||||
return done(null, false, { errors: { 'E-Mail-Bestätigung': 'noch nicht erfolgt' } });
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
|
@ -27,3 +30,153 @@ passport.use(
|
|||
},
|
||||
),
|
||||
);
|
||||
|
||||
function getRequestToken(req) {
|
||||
const authorization = req.headers.authorization;
|
||||
if (typeof authorization !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [tokenType, token] = authorization.split(' ');
|
||||
|
||||
if (tokenType === 'Token' || tokenType === 'Bearer') {
|
||||
return token;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
passport.use(
|
||||
'jwt',
|
||||
new JwtStrategy(
|
||||
{
|
||||
secretOrKey: secret,
|
||||
jwtFromRequest: getRequestToken,
|
||||
algorithms: ['HS256'],
|
||||
},
|
||||
async function (token, done) {
|
||||
try {
|
||||
// we used to put the user ID into the token directly :(
|
||||
const {id} = token
|
||||
const user = await User.findById(id);
|
||||
return done(null, user || false);
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
passport.use(
|
||||
'accessToken',
|
||||
new BearerStrategy(async function (token, done) {
|
||||
try {
|
||||
const accessToken = await AccessToken.findOne({ token }).populate('user');
|
||||
if (accessToken && accessToken.user) {
|
||||
// TODO: scope
|
||||
return done(null, user, { scope: accessToken.scope });
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
passport.use(
|
||||
'refreshToken',
|
||||
new BearerStrategy(async function (token, done) {
|
||||
try {
|
||||
const refreshToken = await RefreshToken.findOne({ token }).populate('user');
|
||||
if (refreshToken && refreshToken.user) {
|
||||
// TODO: scope
|
||||
return done(null, user, { scope: 'auth.refresh' });
|
||||
} else {
|
||||
return done(null, false);
|
||||
}
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
passport.use(
|
||||
'userId',
|
||||
new CustomStrategy(async (req, callback) => {
|
||||
try {
|
||||
let userId;
|
||||
|
||||
const headerToken = getRequestToken(req);
|
||||
if (headerToken && headerToken.length === 24) {
|
||||
userId = headerToken;
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
const bodyId = req.body && req.body.id;
|
||||
if (bodyId && bodyId.length === 24) {
|
||||
userId = bodyId;
|
||||
}
|
||||
}
|
||||
|
||||
let user;
|
||||
if (userId) {
|
||||
user = await User.findById(userId);
|
||||
}
|
||||
|
||||
callback(null, user || false);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* This function creates a middleware that does a passport authentication.
|
||||
*/
|
||||
function createMiddleware(strategies, required) {
|
||||
return (req, res, next) => {
|
||||
passport.authenticate(strategies, { session: false }, (err, user, info) => {
|
||||
// If this authentication produced an error, throw it. In a chain of
|
||||
// multiple strategies, errors are ignored, unless every strategy errors.
|
||||
if (required && err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// If you *must* be logged in for this action, require a user.
|
||||
if (required && !user) {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
|
||||
// Regardless of whether login is required, if you're logged in as an
|
||||
// unverified user, produce an error.
|
||||
if (user && user.needsEmailValidation) {
|
||||
return res.status(403).json({ errors: { 'E-Mail-Bestätigung': 'noch nicht erfolgt' } });
|
||||
}
|
||||
|
||||
req.user = user
|
||||
req.authInfo = info
|
||||
req.scope = (info && info.scope) || '*'
|
||||
|
||||
return next();
|
||||
})(req, res, next);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// these are the standard authentication mechanisms, for when you want user
|
||||
// information in the route, and either require a login, or don't care
|
||||
optional: createMiddleware(['jwt', 'accessToken'], false),
|
||||
required: createMiddleware(['jwt', 'accessToken'], true),
|
||||
|
||||
// required to check username and passwort for generating a new token, e.g.
|
||||
// on the /users/login route, and later on oauth routes
|
||||
usernameAndPassword: createMiddleware('usernameAndPassword', true),
|
||||
|
||||
// will be used to verify a refresh token on the route that will exchange the
|
||||
// refresh token for a new access token (not in use yet)
|
||||
refreshToken: createMiddleware('refreshToken', true),
|
||||
|
||||
// for track upload, we still allow "userId" for a while
|
||||
requiredWithUserId: createMiddleware(['jwt', 'accessToken', 'userId'], true),
|
||||
};
|
||||
|
|
|
@ -4,16 +4,18 @@ const bodyParser = require('body-parser');
|
|||
const session = require('express-session');
|
||||
const cors = require('cors');
|
||||
const errorhandler = require('errorhandler');
|
||||
const auth = require('./routes/auth');
|
||||
const passport = require('passport');
|
||||
|
||||
require('./config/passport')
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
// Create global app object
|
||||
const app = express();
|
||||
|
||||
|
||||
app.use(cors());
|
||||
app.use(auth.getUserIdMiddleware);
|
||||
app.use(auth.loadUserMiddleware);
|
||||
app.use(passport.initialize());
|
||||
|
||||
// Normal express config defaults
|
||||
app.use(require('morgan')('dev'));
|
||||
|
|
47
api/src/models/AccessToken.js
Normal file
47
api/src/models/AccessToken.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
const mongoose = require('mongoose');
|
||||
const uniqueValidator = require('mongoose-unique-validator');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const schema = new mongoose.Schema(
|
||||
{
|
||||
token: { index: true, type: String, required: true, unique: true },
|
||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
expiresAt: { type: Date, required: true },
|
||||
scope: { type: String, required: true, defaultValue: '*' },
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
schema.plugin(uniqueValidator, { message: 'reused token' });
|
||||
|
||||
class AccessToken extends mongoose.Model {
|
||||
toJSON() {
|
||||
return {
|
||||
token: this.token,
|
||||
expires: this.expires,
|
||||
};
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.expiresAt < new Date()
|
||||
}
|
||||
|
||||
toHeaderString() {
|
||||
return 'Bearer ' + this.token;
|
||||
}
|
||||
|
||||
static generate(user, scope = '*', expiresInSeconds = 24 * 60 * 60) {
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
return new AccessToken({
|
||||
token,
|
||||
user,
|
||||
expiresAt: new Date(new Date().getTime() + 1000 * expiresInSeconds),
|
||||
scope,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mongoose.model(AccessToken, schema);
|
||||
|
||||
module.exports = AccessToken;
|
49
api/src/models/RefreshToken.js
Normal file
49
api/src/models/RefreshToken.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
const mongoose = require('mongoose');
|
||||
const uniqueValidator = require('mongoose-unique-validator');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const AccessToken = require('./AccessToken')
|
||||
|
||||
const schema = new mongoose.Schema(
|
||||
{
|
||||
token: { index: true, type: String, required: true, unique: true },
|
||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
expiresAt: { type: Date, required: false },
|
||||
scope: { type: String, required: true, defaultValue: '*' },
|
||||
},
|
||||
{ timestamps: true },
|
||||
);
|
||||
|
||||
schema.plugin(uniqueValidator, { message: 'reused token' });
|
||||
|
||||
class RefreshToken extends mongoose.Model {
|
||||
toJSON() {
|
||||
return {
|
||||
token: this.token,
|
||||
expires: this.expires,
|
||||
};
|
||||
}
|
||||
|
||||
isValid() {
|
||||
return this.expiresAt == null || this.expiresAt < new Date()
|
||||
}
|
||||
|
||||
static generate(user, scope = '*', expiresInSeconds = 24 * 60 * 60) {
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
|
||||
return new RefreshToken({
|
||||
token,
|
||||
user,
|
||||
expiresAt: new Date(new Date().getTime() + 1000 * expiresInSeconds),
|
||||
scope,
|
||||
});
|
||||
}
|
||||
|
||||
genererateAccessToken(expiresInSeconds = undefined) {
|
||||
return AccessToken.generate(this.user, this.scope, expiresInSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
mongoose.model(RefreshToken, schema);
|
||||
|
||||
module.exports = RefreshToken;
|
6
api/src/models/index.js
Normal file
6
api/src/models/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports.AccessToken = require('./AccessToken')
|
||||
module.exports.Comment = require('./Comment')
|
||||
module.exports.RefreshToken = require('./RefreshToken')
|
||||
module.exports.Track = require('./Track')
|
||||
module.exports.TrackData = require('./TrackData')
|
||||
module.exports.User = require('./User')
|
|
@ -2,7 +2,7 @@ const router = require('express').Router();
|
|||
const mongoose = require('mongoose');
|
||||
const User = mongoose.model('User');
|
||||
const wrapRoute = require('../../_helpers/wrapRoute');
|
||||
const auth = require('../auth');
|
||||
const auth = require('../../config/passport');
|
||||
|
||||
// Preload user profile on routes with ':username'
|
||||
router.param('username', async function (req, res, next, username) {
|
||||
|
|
|
@ -5,7 +5,7 @@ const Track = mongoose.model('Track');
|
|||
const Comment = mongoose.model('Comment');
|
||||
const User = mongoose.model('User');
|
||||
const busboy = require('connect-busboy');
|
||||
const auth = require('../auth');
|
||||
const auth = require('../../config/passport');
|
||||
const { normalizeUserAgent, buildObsver1 } = require('../../logic/tracks');
|
||||
const wrapRoute = require('../../_helpers/wrapRoute');
|
||||
|
||||
|
@ -172,7 +172,7 @@ async function getMultipartOrJsonBody(req, mapJsonBody = (x) => x) {
|
|||
|
||||
router.post(
|
||||
'/',
|
||||
auth.required,
|
||||
auth.requiredWithUserId,
|
||||
busboy(), // parse multipart body
|
||||
wrapRoute(async (req, res) => {
|
||||
// Read the whole file into memory. This is not optimal, instead, we should
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const router = require('express').Router();
|
||||
const passport = require('passport');
|
||||
const wrapRoute = require('../../_helpers/wrapRoute');
|
||||
const auth = require('../auth');
|
||||
const auth = require('../../config/passport');
|
||||
|
||||
router.get(
|
||||
'/user',
|
||||
|
@ -42,27 +42,11 @@ router.put(
|
|||
}),
|
||||
);
|
||||
|
||||
router.post('/users/login', function (req, res, next) {
|
||||
if (!req.body.user.email) {
|
||||
return res.status(422).json({ errors: { email: "can't be blank" } });
|
||||
}
|
||||
|
||||
if (!req.body.user.password) {
|
||||
return res.status(422).json({ errors: { password: "can't be blank" } });
|
||||
}
|
||||
|
||||
passport.authenticate('local', { session: false }, function (err, user, info) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (user) {
|
||||
user.token = user.generateJWT();
|
||||
return res.json({ user: user.toAuthJSON() });
|
||||
} else {
|
||||
return res.status(422).json(info);
|
||||
}
|
||||
})(req, res, next);
|
||||
});
|
||||
router.post('/users/login',
|
||||
auth.usernameAndPassword,
|
||||
wrapRoute((req, res) => {
|
||||
return res.json({ user: req.user.toAuthJSON() });
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
const jwt = require('express-jwt');
|
||||
const passport = require('passport')
|
||||
const {LocalStrategy} = require('passport-local')
|
||||
const secret = require('../config').secret;
|
||||
const User = require('../models/User');
|
||||
|
||||
|
@ -27,19 +28,13 @@ async function getUserIdMiddleware(req, res, next) {
|
|||
const [tokenType, token] = (authorization && authorization.split(' ')) || [];
|
||||
|
||||
if (tokenType === 'Token' || tokenType === 'Bearer') {
|
||||
|
||||
// only parse the token as jwt if it looks like one, otherwise we get an error
|
||||
return jwtOptional(req, res, next);
|
||||
|
||||
} else if (tokenType === 'OBSUserId') {
|
||||
req.authInfo = { id: token.trim() };
|
||||
next();
|
||||
} else if (!authorization && req.body && req.body.id && req.body.id.length === 24) {
|
||||
const user = await User.findById(req.body.id);
|
||||
if (user) {
|
||||
req.authInfo = { id: user.id };
|
||||
req.user = user;
|
||||
}
|
||||
next();
|
||||
} else {
|
||||
req.authInfo = null;
|
||||
next();
|
||||
}
|
||||
|
@ -48,33 +43,3 @@ async function getUserIdMiddleware(req, res, next) {
|
|||
}
|
||||
}
|
||||
|
||||
async function loadUserMiddleware(req, res, next) {
|
||||
try {
|
||||
if (req.authInfo && req.authInfo.id) {
|
||||
req.user = await User.findById(req.authInfo.id);
|
||||
|
||||
if (!req.user) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
required(req, res, next) {
|
||||
if (!req.authInfo) {
|
||||
return res.sendStatus(403);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
},
|
||||
optional(req, res, next) {
|
||||
return next();
|
||||
},
|
||||
getUserIdMiddleware,
|
||||
loadUserMiddleware,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue