UNPKG

@nephele/authenticator-custom

Version:

Custom logic authenticator for the Nephele WebDAV server.

158 lines 7.89 kB
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