@nephele/authenticator-htpasswd
Version:
Apache htpasswd based authenticator for the Nephele WebDAV server.
134 lines • 5.39 kB
JavaScript
import crypto from 'node:crypto';
import fsp from 'node:fs/promises';
import { constants } from 'node:fs';
import basicAuth from 'basic-auth';
import apacheMD5 from 'apache-md5';
import crypt from 'apache-crypt';
import bcrypt from 'bcrypt';
import { ForbiddenError, InternalServerError, UnauthorizedError, ResourceNotFoundError, } from 'nephele';
import User from './User.js';
export default class Authenticator {
constructor({ realm = 'Nephele WebDAV Service', unauthorizedAccess = false, authUserFilename = '.htpasswd', authUserFile = undefined, blockAuthUserFilename = true, } = {}) {
this.realm = realm;
this.unauthorizedAccess = unauthorizedAccess;
this.authUserFilename = authUserFilename;
this.authUserFile = authUserFile;
this.blockAuthUserFilename = blockAuthUserFilename;
}
async authenticate(request, response) {
const authorization = request.get('Authorization');
let username = '';
let password = '';
if (this.authUserFile == null &&
this.blockAuthUserFilename &&
(request.path.endsWith(`/${this.authUserFilename}`) ||
request.path.endsWith(`/${this.authUserFilename}/`))) {
throw new ForbiddenError("You don't have permission to access this resource.");
}
if (authorization) {
const auth = basicAuth.parse(authorization);
if (auth) {
username = auth.name;
password = auth.pass;
}
}
try {
if (username.trim() === '') {
throw new UnauthorizedError('Authentication is required to use this server.');
}
const htpasswd = await this._getHtpasswdFile(request, response);
if (!(await this._checkHtpasswd(username, password, htpasswd))) {
throw new UnauthorizedError('The provided credentials are not correct.');
}
return new User({ username });
}
catch (e) {
if (e instanceof UnauthorizedError) {
response.set('WWW-Authenticate', `Basic realm="${this.realm}", charset="UTF-8"`);
}
if (this.unauthorizedAccess) {
return new User({ username: 'nobody' });
}
throw e;
}
}
async cleanAuthentication(_request, _response) {
return;
}
async _checkHtpasswd(username, password, htpasswd) {
let lines = htpasswd.split('\n');
for (let line of lines) {
const [user, digest, extra] = line.split(':');
if (extra) {
continue;
}
if (user === username) {
return await this._checkPassword(digest, password);
}
}
return false;
}
async _checkPassword(digest, password) {
if (digest.startsWith('$apr1$')) {
return digest === apacheMD5(password, digest);
}
else if (digest.startsWith('{SHA}')) {
let hash = crypto.createHash('sha1');
hash.update(password);
return '{SHA}' + hash.digest('base64') === digest;
}
else if (digest.startsWith('$2y$')) {
return await bcrypt.compare(password, '$2b$' + digest.substring(4));
}
return digest === password || crypt(password, digest) === digest;
}
async _getHtpasswdFile(request, response) {
if (this.authUserFile != null) {
try {
await fsp.access(this.authUserFile, constants.F_OK);
return await fsp.readFile(this.authUserFile, { encoding: 'utf-8' });
}
catch (e) {
throw new InternalServerError("The server's authentication user file is not accessible.");
}
}
const adapter = response.locals.adapter;
const baseUrl = response.locals.baseUrl;
const urlParts = request.path
.substring(baseUrl.pathname.length)
.replace(/(?:^\/|\/$)/g, '')
.split('/');
for (let i = 0; i < urlParts.length; i++) {
const htpasswdUrl = new URL(`${[...urlParts.slice(0, i), ''].join('/')}${this.authUserFilename}`, baseUrl);
let htpasswdResource = undefined;
try {
htpasswdResource = await adapter.getResource(htpasswdUrl, baseUrl);
}
catch (e) {
if (e instanceof ResourceNotFoundError) {
continue;
}
throw e;
}
if (htpasswdResource != null) {
try {
const stream = await htpasswdResource.getStream();
return await this._streamToString(stream);
}
catch (e) {
throw new InternalServerError("The server's authentication user file is not accessible.");
}
}
}
return '';
}
_streamToString(stream) {
const chunks = [];
return new Promise((resolve, reject) => {
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
stream.on('error', (err) => reject(err));
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
});
}
}
//# sourceMappingURL=Authenticator.js.map