mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-01-04 07:02:45 +08:00
commit
32e6d3cc10
7 changed files with 283 additions and 14 deletions
|
@ -296,7 +296,8 @@
|
|||
},
|
||||
|
||||
"authentication": {
|
||||
"read:own": ["*"]
|
||||
"read:own": ["*"],
|
||||
"create:own": ["*"]
|
||||
},
|
||||
|
||||
"userlisting": {
|
||||
|
|
|
@ -638,6 +638,67 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AuthenticateResponse'
|
||||
|
||||
/preauth:
|
||||
post:
|
||||
tags:
|
||||
- Authentication
|
||||
summary: Pre-auth check
|
||||
description: Check if an username exists and can be used for authentication
|
||||
operationId: preauth
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- username
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: Username or E-mail address
|
||||
scope:
|
||||
type: string
|
||||
description: 'Required scope. One of master, imap, smtp, pop3'
|
||||
sess:
|
||||
type: string
|
||||
description: Session identifier for the logs
|
||||
ip:
|
||||
type: string
|
||||
description: IP address for the logs
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Success
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
required:
|
||||
- success
|
||||
- id
|
||||
- username
|
||||
- scope
|
||||
- require2fa
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
description: Indicates successful response
|
||||
id:
|
||||
type: string
|
||||
description: ID of authenticated User
|
||||
username:
|
||||
type: string
|
||||
description: Username of authenticated User
|
||||
scope:
|
||||
type: string
|
||||
description: The scope this authentication is valid for
|
||||
require2fa:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of enabled 2FA mechanisms
|
||||
|
||||
'/users/{user}/authlog':
|
||||
get:
|
||||
tags:
|
||||
|
|
|
@ -4,7 +4,7 @@ const Joi = require('joi');
|
|||
const ObjectId = require('mongodb').ObjectId;
|
||||
const tools = require('../../tools');
|
||||
const roles = require('../../roles');
|
||||
const { sessSchema, sessIPSchema } = require('../../schemas');
|
||||
const { sessSchema, sessIPSchema, booleanSchema } = require('../../schemas');
|
||||
|
||||
module.exports = (db, server, userHandler) => {
|
||||
server.get(
|
||||
|
@ -274,9 +274,9 @@ module.exports = (db, server, userHandler) => {
|
|||
|
||||
// permissions check
|
||||
if (req.user && req.user === result.value.user) {
|
||||
req.validate(roles.can(req.role).updateOwn('users'));
|
||||
req.validate(roles.can(req.role).createOwn('authentication'));
|
||||
} else {
|
||||
req.validate(roles.can(req.role).updateAny('users'));
|
||||
req.validate(roles.can(req.role).createAny('authentication'));
|
||||
}
|
||||
|
||||
let user = new ObjectId(result.value.user);
|
||||
|
@ -319,6 +319,8 @@ module.exports = (db, server, userHandler) => {
|
|||
|
||||
rpId: Joi.string().hostname().empty(''),
|
||||
|
||||
token: booleanSchema.default(false),
|
||||
|
||||
sess: sessSchema,
|
||||
ip: sessIPSchema
|
||||
});
|
||||
|
@ -338,21 +340,45 @@ module.exports = (db, server, userHandler) => {
|
|||
return next();
|
||||
}
|
||||
|
||||
// permissions check
|
||||
let permission;
|
||||
|
||||
if (req.user && req.user === result.value.user) {
|
||||
req.validate(roles.can(req.role).updateOwn('users'));
|
||||
permission = roles.can(req.role).createOwn('authentication');
|
||||
} else {
|
||||
req.validate(roles.can(req.role).updateAny('users'));
|
||||
permission = roles.can(req.role).createAny('authentication');
|
||||
}
|
||||
|
||||
// permissions check
|
||||
req.validate(permission);
|
||||
|
||||
// filter out unallowed fields
|
||||
result.value = permission.filter(result.value);
|
||||
|
||||
let user = new ObjectId(result.value.user);
|
||||
|
||||
let response = await userHandler.webauthnAssertAuthentication(user, result.value);
|
||||
let authData = await userHandler.webauthnAssertAuthentication(user, result.value);
|
||||
|
||||
res.json({
|
||||
let authResponse = {
|
||||
success: true,
|
||||
response
|
||||
});
|
||||
response: authData
|
||||
};
|
||||
|
||||
if (result.value.token) {
|
||||
try {
|
||||
authResponse.token = await userHandler.generateAuthToken(user);
|
||||
} catch (err) {
|
||||
let response = {
|
||||
error: err.message,
|
||||
code: err.code || 'AuthFailed',
|
||||
id: user.toString()
|
||||
};
|
||||
res.status(403);
|
||||
res.json(response);
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
res.json(permission.filter(authResponse));
|
||||
|
||||
return next();
|
||||
})
|
||||
|
|
|
@ -8,6 +8,96 @@ const roles = require('../roles');
|
|||
const { nextPageCursorSchema, previousPageCursorSchema, pageNrSchema, sessSchema, sessIPSchema, booleanSchema } = require('../schemas');
|
||||
|
||||
module.exports = (db, server, userHandler) => {
|
||||
server.post(
|
||||
'/preauth',
|
||||
tools.asyncifyJson(async (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
username: Joi.alternatives()
|
||||
.try(
|
||||
Joi.string()
|
||||
.lowercase()
|
||||
.regex(/^[a-z0-9][a-z0-9.]+[a-z0-9]$/, 'username')
|
||||
.min(3)
|
||||
.max(30),
|
||||
Joi.string().email({ tlds: false })
|
||||
)
|
||||
.required(),
|
||||
|
||||
scope: Joi.string().default('master'),
|
||||
|
||||
sess: sessSchema,
|
||||
ip: sessIPSchema
|
||||
});
|
||||
|
||||
const result = schema.validate(req.params, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.status(400);
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError',
|
||||
details: tools.validationErrors(result)
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let permission = roles.can(req.role).createAny('authentication');
|
||||
|
||||
// permissions check
|
||||
req.validate(permission);
|
||||
|
||||
// filter out unallowed fields
|
||||
result.value = permission.filter(result.value);
|
||||
|
||||
let authData, user;
|
||||
|
||||
try {
|
||||
[authData, user] = await userHandler.preAuth(result.value.username, result.value.scope);
|
||||
} catch (err) {
|
||||
let response = {
|
||||
error: err.message,
|
||||
code: err.code || 'AuthFailed'
|
||||
};
|
||||
if (user) {
|
||||
response.id = user.toString();
|
||||
}
|
||||
res.status(403);
|
||||
res.json(response);
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!authData) {
|
||||
let response = {
|
||||
error: 'Authentication failed',
|
||||
code: 'AuthFailed'
|
||||
};
|
||||
if (user) {
|
||||
response.id = user.toString();
|
||||
}
|
||||
res.status(403);
|
||||
res.json(response);
|
||||
return next();
|
||||
}
|
||||
|
||||
let preAuthResponse = {
|
||||
success: true,
|
||||
id: authData.user.toString(),
|
||||
username: authData.username,
|
||||
scope: authData.scope,
|
||||
require2fa: authData.require2fa
|
||||
};
|
||||
|
||||
res.status(200);
|
||||
res.json(permission.filter(preAuthResponse));
|
||||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
server.post(
|
||||
'/authenticate',
|
||||
tools.asyncifyJson(async (req, res, next) => {
|
||||
|
@ -80,11 +170,13 @@ module.exports = (db, server, userHandler) => {
|
|||
} catch (err) {
|
||||
let response = {
|
||||
error: err.message,
|
||||
code: 'AuthFailed' || err.code
|
||||
code: err.code || 'AuthFailed'
|
||||
};
|
||||
|
||||
if (user) {
|
||||
response.id = user.toString();
|
||||
}
|
||||
|
||||
res.status(403);
|
||||
res.json(response);
|
||||
return next();
|
||||
|
|
|
@ -1048,6 +1048,71 @@ class UserHandler {
|
|||
}
|
||||
}
|
||||
|
||||
async preAuth(username, requiredScope) {
|
||||
requiredScope = requiredScope || 'master';
|
||||
|
||||
username = (username || '').toString();
|
||||
|
||||
let userQuery;
|
||||
try {
|
||||
userQuery = await this.checkAddress(username);
|
||||
} catch (err) {
|
||||
return [false, false];
|
||||
}
|
||||
|
||||
if (!userQuery) {
|
||||
// nothing to do here
|
||||
return [false, false];
|
||||
}
|
||||
|
||||
let userData;
|
||||
try {
|
||||
userData = await this.users.collection('users').findOne(userQuery, {
|
||||
projection: {
|
||||
_id: true,
|
||||
username: true,
|
||||
address: true,
|
||||
enabled2fa: true,
|
||||
webauthn: true,
|
||||
disabled: true,
|
||||
suspended: true,
|
||||
disabledScopes: true
|
||||
},
|
||||
maxTimeMS: consts.DB_MAX_TIME_USERS
|
||||
});
|
||||
} catch (err) {
|
||||
// return as failed auth
|
||||
return [false, false];
|
||||
}
|
||||
|
||||
if (!userData || userData.disabled || userData.suspended) {
|
||||
// return as failed auth
|
||||
return [false, false];
|
||||
}
|
||||
|
||||
let disabledScopes = userData.disabledScopes || [];
|
||||
if (disabledScopes.includes(requiredScope)) {
|
||||
let err = new Error('Access to requested service disabled');
|
||||
err.response = 'NO';
|
||||
err.responseCode = 403;
|
||||
err.code = 'InvalidAuthScope';
|
||||
err.user = userData._id;
|
||||
throw err;
|
||||
}
|
||||
|
||||
let enabled2fa = tools.getEnabled2fa(userData.enabled2fa);
|
||||
|
||||
let authResponse = {
|
||||
user: userData._id,
|
||||
username: userData.username,
|
||||
scope: requiredScope,
|
||||
// if 2FA is enabled then require token validation
|
||||
require2fa: requiredScope === 'master' && enabled2fa.length ? enabled2fa : false
|
||||
};
|
||||
|
||||
return [authResponse, userData._id];
|
||||
}
|
||||
|
||||
async generateASP(user, data) {
|
||||
let password =
|
||||
data.password ||
|
||||
|
@ -2646,7 +2711,7 @@ class UserHandler {
|
|||
.filter(credentialData => credentialData.authenticatorAttachment === data.authenticatorAttachment)
|
||||
.map(credentialData => ({
|
||||
rawId: credentialData.rawId.toString('hex'),
|
||||
type: credentialData.type,
|
||||
type: credentialData.type
|
||||
}));
|
||||
|
||||
// store chalenge
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"@root/csr": "0.8.1",
|
||||
"accesscontrol": "2.2.1",
|
||||
"argon2-browser": "1.18.0",
|
||||
"axios": "1.0.0",
|
||||
"axios": "1.1.0",
|
||||
"base32.js": "0.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"bson": "4.7.0",
|
||||
|
|
|
@ -133,6 +133,30 @@ describe('API tests', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('preauth', () => {
|
||||
it('should POST /preauth with success', async () => {
|
||||
const response = await server
|
||||
.post(`/preauth`)
|
||||
.send({
|
||||
username: 'testuser@example.com',
|
||||
scope: 'master'
|
||||
})
|
||||
.expect(200);
|
||||
expect(response.body.success).to.be.true;
|
||||
});
|
||||
|
||||
it('should POST /preauth using alias domain', async () => {
|
||||
const response = await server
|
||||
.post(`/preauth`)
|
||||
.send({
|
||||
username: 'testuser@jõgeva.öö',
|
||||
scope: 'master'
|
||||
})
|
||||
.expect(200);
|
||||
expect(response.body.success).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('asp', () => {
|
||||
it('should POST /users/:user/asps to generate ASP', async () => {
|
||||
const response = await server
|
||||
|
|
Loading…
Reference in a new issue