Add passportjs and refactor login

Signed-off-by: Alexandre Bodin <bodin.alex@gmail.com>
This commit is contained in:
Alexandre Bodin 2020-05-11 17:09:48 +02:00
parent 7c305e3e8c
commit ceb11379fc
20 changed files with 248 additions and 108 deletions

View File

@ -1,4 +1,9 @@
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
admin: {
jwt: {
secret: env('ADMIN_JWT_SECRET', 'cdd07276439366dcc133324e14a1d6cb'),
},
},
});

View File

@ -56,8 +56,8 @@
},
{
"method": "POST",
"path": "/auth/local",
"handler": "Auth.callback"
"path": "/login",
"handler": "authentication.login"
},
{
"method": "POST",

View File

@ -16,89 +16,6 @@ const formatError = error => [
];
module.exports = {
async callback(ctx) {
const params = ctx.request.body;
// The identifier is required.
if (!params.identifier) {
return ctx.badRequest(
null,
formatError({
id: 'Auth.form.error.email.provide',
message: 'Please provide your username or your e-mail.',
})
);
}
// The password is required.
if (!params.password) {
return ctx.badRequest(
null,
formatError({
id: 'Auth.form.error.password.provide',
message: 'Please provide your password.',
})
);
}
const query = {};
// Check if the provided identifier is an email or not.
const isEmail = emailRegExp.test(params.identifier);
// Set the identifier to the appropriate query field.
if (isEmail) {
query.email = params.identifier.toLowerCase();
} else {
query.username = params.identifier;
}
// Check if the admin exists.
const admin = await strapi.query('user', 'admin').findOne(query);
if (!admin) {
return ctx.badRequest(
null,
formatError({
id: 'Auth.form.error.invalid',
message: 'Identifier or password invalid.',
})
);
}
if (admin.blocked === true) {
return ctx.badRequest(
null,
formatError({
id: 'Auth.form.error.blocked',
message: 'Your account has been blocked by the administrator.',
})
);
}
const validPassword = await strapi.admin.services.auth.validatePassword(
params.password,
admin.password
);
if (!validPassword) {
return ctx.badRequest(
null,
formatError({
id: 'Auth.form.error.invalid',
message: 'Identifier or password invalid.',
})
);
} else {
admin.isAdmin = true;
ctx.send({
jwt: strapi.admin.services.auth.createJwtToken(admin),
user: strapi.admin.services.auth.sanitizeUser(admin),
});
}
},
async register(ctx) {
const params = ctx.request.body;

View File

@ -0,0 +1,33 @@
'use strict';
const passport = require('koa-passport');
const compose = require('koa-compose');
const login = compose([
(ctx, next) => {
return passport.authenticate('local', { session: false }, (err, user, info) => {
if (err) {
ctx.body = { error: 'Internal server error' };
} else if (!user) {
ctx.body = { error: info.error };
} else {
ctx.state.user = user;
return next();
}
})(ctx, next);
},
ctx => {
const { user } = ctx.state;
ctx.body = {
data: {
token: strapi.admin.services.auth.createJwtToken(user),
user: strapi.admin.services.auth.sanitizeUser(ctx.state.user), // TODO: fetch more detailed info
},
};
},
]);
module.exports = {
login,
};

View File

@ -0,0 +1,5 @@
{
"auth": {
"enabled": true
}
}

View File

@ -0,0 +1,57 @@
'use strict';
const passport = require('koa-passport');
const { Strategy: LocalStrategy } = require('passport-local');
const { Strategy: JwtStrategy, ExtractJwt } = require('passport-jwt');
const createLocalStrategy = strapi => {
return new LocalStrategy(
{
usernameField: 'email',
passwordField: 'password',
session: false,
},
function(email, password, done) {
return strapi.admin.services.auth
.checkCredentials({ email, password })
.then(([error, user, message]) => done(error, user, message))
.catch(err => done(err));
}
);
};
const createJWTStrategy = strapi => {
const { options, secret } = strapi.admin.services.auth.getJWTOptions();
const opts = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: secret,
jsonWebTokenOptions: options,
};
return new JwtStrategy(opts, function({ id }, done) {
strapi
.query('administrator', 'admin')
.findOne({ id })
.then(user => {
if (user) {
return done(null, user);
} else {
return done(null, false);
}
})
.catch(err => {
return done(err, false);
});
});
};
module.exports = strapi => ({
initialize() {
passport.use(createLocalStrategy(strapi));
passport.use(createJWTStrategy(strapi));
// strapi.app.use(passport.authenticate('jwt', { session: false }));
strapi.app.use(passport.initialize());
},
});

View File

@ -57,10 +57,16 @@
"immutable": "^3.8.2",
"invariant": "^2.2.4",
"is-wsl": "^2.0.0",
"jsonwebtoken": "8.5.1",
"koa-compose": "4.1.0",
"koa-passport": "4.1.3",
"lodash": "^4.17.11",
"match-sorter": "^4.0.2",
"mini-css-extract-plugin": "^0.6.0",
"moment": "^2.24.0",
"passport": "0.4.1",
"passport-jwt": "4.0.0",
"passport-local": "1.0.0",
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-copy-to-clipboard": "^5.0.1",

View File

@ -1,11 +1,22 @@
'use strict';
const _ = require('lodash');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const sanitizeUser = user => {
return _.omit(user.toJSON ? user.toJSON() : user, [
'password',
'resetPasswordToken',
]);
return _.omit(user, ['password', 'resetPasswordToken']);
};
const defaultOptions = { expiresIn: '30d' };
const getJWTOptions = () => {
const { options, secret } = strapi.config.get('server.admin.jwt', {});
return {
secret,
options: _.merge(options, defaultOptions),
};
};
/**
@ -13,10 +24,16 @@ const sanitizeUser = user => {
* @param {object} admon - admin user
*/
const createJwtToken = admin => {
return strapi.plugins['users-permissions'].services.jwt.issue({
id: admin.id,
isAdmin: true,
});
const { options, secret } = getJWTOptions();
return jwt.sign(
{
id: admin.id,
isAdmin: true,
},
secret,
options
);
};
/**
@ -34,9 +51,38 @@ const hashPassword = password => bcrypt.hash(password, 10);
*/
const validatePassword = (password, hash) => bcrypt.compare(password, hash);
/**
* Check login credentials
* @param {Object} options
* @param {string} options.email
* @param {string} options.password
*/
const checkCredentials = async ({ email, password }) => {
const user = await strapi.query('administrator', 'admin').findOne({ email });
if (!user) {
return [null, false, { error: 'Invalid credentials' }];
}
const isValid = await strapi.admin.services.auth.validatePassword(password, user.password);
if (!isValid) {
return [null, false, { error: 'Invalid credentials' }];
}
// TODO: change to isActive
if (user.blocked === true) {
return [null, false, { error: 'User not active' }];
}
return [null, user];
};
module.exports = {
checkCredentials,
createJwtToken,
sanitizeUser,
validatePassword,
hashPassword,
getJWTOptions,
};

View File

@ -10,7 +10,8 @@ const _ = require('lodash');
const stopProcess = require('./utils/stop-process');
const { trackUsage, captureStderr } = require('./utils/usage');
const packageJSON = require('./resources/json/package.json');
const databaseJSON = require('./resources/json/database.json.js');
const createDatabaseConfig = require('./resources/templates/database.js');
const createServerConfig = require('./resources/templates/server.js');
module.exports = async function createProject(scope, { client, connection, dependencies }) {
console.log('Creating files.');
@ -52,14 +53,18 @@ module.exports = async function createProject(scope, { client, connection, depen
// ensure node_modules is created
await fse.ensureDir(join(rootPath, 'node_modules'));
// create config/database.js
await fse.writeFile(
join(rootPath, `config/database.js`),
databaseJSON({
createDatabaseConfig({
client,
connection,
})
);
// create config/server.js
await fse.writeFile(join(rootPath, `config/server.js`), createServerConfig());
await trackUsage({ event: 'didCopyConfigurationFiles', scope });
} catch (err) {
await fse.remove(scope.rootPath);

View File

@ -0,0 +1,16 @@
'use strict';
const _ = require('lodash');
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
module.exports = () => {
const tmpl = fs.readFileSync(path.join(__dirname, `server.template`));
const compile = _.template(tmpl);
return compile({
adminJwtToken: crypto.randomBytes(16).toString('hex'),
});
};

View File

@ -1,4 +1,9 @@
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
admin: {
jwt: {
secret: env('ADMIN_JWT_SECRET', '<%= adminJwtToken %>'),
},
},
});

View File

@ -17,7 +17,6 @@ module.exports = async function(strapi) {
const loaders = createLoaders(strapi);
// load installed middlewares
await loaders.loadMiddlewareDependencies(installedMiddlewares, middlewares);
// internal middlewares
await loaders.loadInternalMiddlexares(middlewares);
@ -27,6 +26,8 @@ module.exports = async function(strapi) {
await loaders.loadPluginsMiddlewares(installedPlugins, middlewares);
// local plugin middlewares
await loaders.loadLocalPluginsMiddlewares(appPath, middlewares);
// load admin middlwares
await loaders.loadAdminMiddlewares(middlewares);
return middlewares;
};
@ -76,6 +77,11 @@ const createLoaders = strapi => {
}
};
const loadAdminMiddlewares = async middlewares => {
const dir = path.resolve(findPackagePath(`strapi-admin`), 'middlewares');
await loadMiddlewaresInDir(dir, middlewares);
};
const loadMiddlewareDependencies = async (packages, middlewares) => {
for (let packageName of packages) {
const baseDir = path.dirname(require.resolve(`strapi-middleware-${packageName}`));
@ -113,5 +119,6 @@ const createLoaders = strapi => {
loadPluginsMiddlewares,
loadLocalPluginsMiddlewares,
loadMiddlewareDependencies,
loadAdminMiddlewares,
};
};

View File

@ -27,15 +27,13 @@ module.exports = strapi => {
strapi.router.prefix(strapi.config.get('middleware.settings.router.prefix', ''));
if (!_.isEmpty(_.get(strapi.admin, 'config.routes', false))) {
// Create router for admin.
// Prefix router with the admin's name.
if (_.has(strapi.admin, 'config.routes')) {
const router = new Router({
prefix: '/admin',
});
_.forEach(strapi.admin.config.routes, value => {
composeEndpoint(value, { router });
_.get(strapi.admin, 'config.routes', []).forEach(route => {
composeEndpoint(route, { router });
});
// Mount admin router on Strapi router

View File

@ -30,7 +30,7 @@ module.exports = strapi =>
controller = strapi.controllers[controllerKey] || strapi.admin.controllers[controllerKey];
}
const action = controller[actionName].bind(controller);
const action = controller[actionName];
// Retrieve the API's name where the controller is located
// to access to the right validators

View File

@ -10627,7 +10627,7 @@ jsonparse@^1.2.0:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=
jsonwebtoken@^8.1.0:
jsonwebtoken@8.5.1, jsonwebtoken@^8.1.0, jsonwebtoken@^8.2.0:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
@ -10777,6 +10777,11 @@ koa-bodyparser@^4.2.1:
co-body "^6.0.0"
copy-to "^2.0.1"
koa-compose@4.1.0, koa-compose@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
koa-compose@^3.0.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
@ -10784,11 +10789,6 @@ koa-compose@^3.0.0:
dependencies:
any-promise "^1.1.0"
koa-compose@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
koa-compose@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-2.3.0.tgz#4617fa832a16412a56967334304efd797d6ed35c"
@ -10863,6 +10863,13 @@ koa-lusca@~2.2.0:
dependencies:
koa-compose "~2.3.0"
koa-passport@4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/koa-passport/-/koa-passport-4.1.3.tgz#6e8eaa48290457af1539bcbed3c52d9defc029c9"
integrity sha512-QqKrHfp4jNfqkKThGkVb2WQtlVOSRYk5CC69Z17cmOpZ4760l8CdyJ+Bs2CfQc9BHizz557mczjPkSlVkpuluw==
dependencies:
passport "^0.4.0"
koa-range@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/koa-range/-/koa-range-0.3.0.tgz#3588e3496473a839a1bd264d2a42b1d85bd7feac"
@ -13256,6 +13263,34 @@ pascalcase@^0.1.1:
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
passport-jwt@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/passport-jwt/-/passport-jwt-4.0.0.tgz#7f0be7ba942e28b9f5d22c2ebbb8ce96ef7cf065"
integrity sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==
dependencies:
jsonwebtoken "^8.2.0"
passport-strategy "^1.0.0"
passport-local@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-local/-/passport-local-1.0.0.tgz#1fe63268c92e75606626437e3b906662c15ba6ee"
integrity sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=
dependencies:
passport-strategy "1.x.x"
passport-strategy@1.x.x, passport-strategy@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4"
integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=
passport@0.4.1, passport@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270"
integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==
dependencies:
passport-strategy "1.x.x"
pause "0.0.1"
path-browserify@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
@ -13372,6 +13407,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pause@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d"
integrity sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=
pbkdf2@^3.0.3:
version "3.0.17"
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6"