@nephele/authenticator-custom
Version:
Custom logic authenticator for the Nephele WebDAV server.
158 lines • 7.89 kB
JavaScript
import crypto from 'node:crypto';
import { parseAuthorizationHeader, BASIC, DIGEST, } from 'http-auth-utils-hperrin';
import { v4 as uuid } from 'uuid';
import { UnauthorizedError } from 'nephele';
import User from './User.js';
class StaleUnauthorizedError extends UnauthorizedError {
}
function hash(input, algorithm) {
return crypto.createHash(algorithm).update(input).digest('hex').toLowerCase();
}
export default class Authenticator {
constructor(config) {
this.getUser = config.getUser;
if ('authBasic' in config) {
this.authBasic = config.authBasic;
}
if ('authDigest' in config) {
this.authDigest = config.authDigest;
}
if (this.authBasic == null && this.authDigest == null) {
throw new Error('You must provide at least one auth function to authenticator-custom.');
}
this.realm = config.realm || 'Nephele WebDAV Service';
this.unauthorizedAccess = config.unauthorizedAccess || false;
this.key = config.key || uuid();
this.nonceTimeout = config.nonceTimeout || 1000 * 60 * 60 * 6;
}
async authenticate(request, response) {
const authorization = request.get('Authorization');
try {
if (authorization) {
const auth = parseAuthorizationHeader(authorization);
if (auth.type === 'Basic' && 'password' in auth.data) {
let { username, password } = auth.data;
if (this.authBasic == null) {
throw new UnauthorizedError('Basic authentication is not supported on this server.');
}
if (!username && !password) {
throw new UnauthorizedError('You must provide a username and password to access this server.');
}
const user = await this.getUser(username);
if (user == null) {
throw new UnauthorizedError('The provided credentials are not correct.');
}
if (!(await this.authBasic(user, password))) {
throw new UnauthorizedError('The provided credentials are not correct.');
}
return user;
}
else if (auth.type === 'Digest' && 'response' in auth.data) {
let { username, realm, nonce, uri, algorithm, response, cnonce, nc, qop, opaque, } = auth.data;
if (this.authDigest == null) {
throw new UnauthorizedError('Digest authentication is not supported on this server.');
}
if (!username) {
throw new UnauthorizedError('You must provide a username to access this server.');
}
if (opaque == null) {
throw new UnauthorizedError('You must provide an opaque value to access this server.');
}
if (realm !== this.realm) {
throw new UnauthorizedError('The provided realm does not match this server.');
}
if (uri !== request.url &&
uri !==
`${request.protocol}://${request.headers.host}${request.url}`) {
throw new UnauthorizedError('The provided auth URI does not match this request.');
}
const timestamp = parseInt(opaque, 16);
if (isNaN(timestamp) ||
timestamp < new Date().getTime() - this.nonceTimeout) {
throw new StaleUnauthorizedError('The provided nonce has expired.');
}
const checkNonce = hash(`${request.ip}:${opaque}:${this.key}`, 'sha256');
if (checkNonce !== nonce) {
throw new StaleUnauthorizedError('The provided nonce was not issued to this client by this server.');
}
const user = await this.getUser(username);
if (user == null) {
throw new UnauthorizedError('The provided credentials are not correct.');
}
if ((algorithm != null &&
algorithm !== 'SHA256-sess' &&
algorithm !== 'MD5-sess') ||
cnonce == null ||
qop === 'auth-int') {
throw new UnauthorizedError('The provided credentials are not in a supported format.');
}
const digestInfo = await this.authDigest(user, this.realm, algorithm === 'MD5-sess' ? 'md5' : 'sha256');
if (digestInfo == null) {
throw new UnauthorizedError('The provided credentials are not correct.');
}
const hashAlg = algorithm === 'MD5-sess' ? 'md5' : 'sha256';
let HA1;
if ('hash' in digestInfo) {
HA1 = hash(`${digestInfo.hash}:${nonce}:${cnonce}`, hashAlg);
}
else {
const { password } = digestInfo;
HA1 = hash(`${hash(`${username}:${this.realm}:${password}`, hashAlg)}:${nonce}:${cnonce}`, hashAlg);
}
let HA2 = hash(`${request.method}:${uri}`, hashAlg);
let check;
if (qop === 'auth') {
check = hash(`${HA1}:${nonce}:${nc}:${cnonce}:${qop}:${HA2}`, hashAlg);
}
else {
check = hash(`${HA1}:${nonce}:${HA2}`, hashAlg);
}
if (check !== response) {
throw new UnauthorizedError('The provided credentials are not correct.');
}
return user;
}
}
throw new UnauthorizedError('You must authenticate to access this server.');
}
catch (e) {
if (e instanceof UnauthorizedError) {
const auths = [];
if (this.authBasic != null) {
auths.push(`Basic ${BASIC.buildWWWAuthenticateRest({
realm: this.realm,
})}, charset="UTF-8"`);
}
if (this.authDigest != null) {
const opaque = new Date().getTime().toString(16);
const nonce = hash(`${request.ip}:${opaque}:${this.key}`, 'sha256');
auths.push(`Digest ${DIGEST.buildWWWAuthenticateRest({
nonce,
opaque,
qop: 'auth',
algorithm: 'SHA256-sess',
realm: this.realm,
stale: e instanceof StaleUnauthorizedError ? 'true' : 'false',
})}, charset="UTF-8"`);
auths.push(`Digest ${DIGEST.buildWWWAuthenticateRest({
nonce,
opaque,
qop: 'auth',
algorithm: 'MD5-sess',
realm: this.realm,
stale: e instanceof StaleUnauthorizedError ? 'true' : 'false',
})}, charset="UTF-8"`);
}
response.set('WWW-Authenticate', auths);
}
if (this.unauthorizedAccess) {
return new User({ username: 'nobody' });
}
throw e;
}
}
async cleanAuthentication(_request, _response) {
return;
}
}
//# sourceMappingURL=Authenticator.js.map