diff --git a/packages/authenticator-custom/README.md b/packages/authenticator-custom/README.md index 158d8e5..953d531 100644 --- a/packages/authenticator-custom/README.md +++ b/packages/authenticator-custom/README.md @@ -26,13 +26,26 @@ app.use( nepheleServer({ adapter: new ExampleAdapter(), authenticator: new CustomAuthenticator({ - auth: async (username, password) => { - if (username === 'admin' && password === 'password') { + getUser: async (username) => { + if (username === 'admin') { const user = new User({ username }); user.someArbitraryPropYouMayNeed = 'somevalue'; return user; } - + return null; + }, + // For Basic authentication. + authBasic: async (user, password) => { + if (user.username === 'admin' && password === 'password') { + return true; + } + return false; + }, + // For Digest authentication. + authDigest: async (user) => { + if (user.username === 'admin') { + return { password: 'password' }; + } return null; }, realm: 'My WebDAV Server', @@ -47,13 +60,33 @@ app.listen(port, () => { # Options / Defaults -- `auth`: A function that takes a username and password and returns a promise that resolves to a user if the authentication succeeds, or null otherwise. +- `getUser`: A function that takes a username and returns a promise that resolves to a user if the user exists or it's not possible to tell whether they exist, or null otherwise. - `realm` = `'Nephele WebDAV Service'`: The realm is the name reported by the server when the user is prompted to authenticate. +- `key` = `random_uuid()`: A private key used to calculate nonce values for Digest authentication. +- `nonceTimeout` = `1000 * 60 * 60 * 6`: The number of milliseconds for which a nonce is valid once issued. Defaults to 6 hours. +- `authBasic`: Authorize a User returned by `getUser` with a password. +- `authDigest`: Retrieve a User's password or hash for Digest authentication. ## realm It should be HTTP header safe (shouldn't include double quotes or semicolon). +## key + +If you do not provide one, one will be generated, but this does mean that with Digest authentication, clients will only be able to authenticate to _that_ particular server. If you have multiple servers or multiple instances of Nephele that serve the same source data, you should provide the same key to all of them in order to use Digest authentication correctly. + +## authBasic + +The returned promise should resolve to true if the user is successfully authenticated, false otherwise. + +The Basic mechanism requires the user to submit their username and password in plain text with the request, so only use this if the connection is secured through some means like TLS. If you provide `authBasic`, the server will advertise support for the Basic mechanism. + +## authDigest + +The returned promise should resolve to the password or hash if the user exists, or null otherwise. If the password is returned, it will be hashed, however, you can also return a prehashed string of SHA256(username:realm:password) or MD5(username:realm:password), depending on the requested algorithm. + +The Digest mechansism requires the user to cryptographically hash their password with the request, so it will not divulge their password to eaves droppers. However, it is still less safe than using TLS and Basic authentication. If you provide `authDigest`, the server will advertise support for the Digest mechanism. + # License Copyright 2022 SciActive Inc diff --git a/packages/authenticator-custom/package-lock.json b/packages/authenticator-custom/package-lock.json index 547a45b..e019b69 100644 --- a/packages/authenticator-custom/package-lock.json +++ b/packages/authenticator-custom/package-lock.json @@ -9,13 +9,14 @@ "version": "1.0.0-alpha.9", "license": "Apache-2.0", "dependencies": { - "basic-auth": "^2.0.1" + "http-auth-utils-hperrin": "^3.0.4", + "uuid": "^8.3.2" }, "devDependencies": { "@tsconfig/recommended": "^1.0.1", - "@types/basic-auth": "^1.1.3", "@types/express": "^4.17.13", "@types/jest": "^27.5.0", + "@types/uuid": "^8.3.4", "express": "^4.18.1", "jest": "^28.1.0", "ts-jest": "^28.0.2", @@ -1018,6 +1019,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@sciactive/splitn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sciactive/splitn/-/splitn-1.0.0.tgz", + "integrity": "sha512-z3CI0SVzAsLYmRvLDS1pOzIX9ztCVXIT9n4MMRGM7GVbu8clZqbXoz3/NytQfyP2KSeYU7YoZYlQpYkQ7KsQ1w==" + }, "node_modules/@sinclair/typebox": { "version": "0.24.27", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.27.tgz", @@ -1089,15 +1095,6 @@ "@babel/types": "^7.3.0" } }, - "node_modules/@types/basic-auth": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/basic-auth/-/basic-auth-1.1.3.tgz", - "integrity": "sha512-W3rv6J0IGlxqgE2eQ2pTb0gBjaGtejQpJ6uaCjz3UQ65+TFTPC5/lAE+POfx1YLdjtxvejJzsIAfd3MxWiVmfg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -1229,6 +1226,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.11", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.11.tgz", @@ -1421,17 +1424,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", @@ -2322,6 +2314,18 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-auth-utils-hperrin": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/http-auth-utils-hperrin/-/http-auth-utils-hperrin-3.0.4.tgz", + "integrity": "sha512-gRV9GMNpT4UTLbCdE4Y+PCjK1vR7tULbKAwJmUGvXsfaCIHMPFVC3HBJSpSGyFxTNJrFxZbc4eK1tFW9zqZznQ==", + "dependencies": { + "@sciactive/splitn": "^1.0.0", + "yerror": "^6.0.2" + }, + "engines": { + "node": ">=16.15.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4117,7 +4121,8 @@ "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -4608,6 +4613,14 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", @@ -4733,6 +4746,14 @@ "node": ">=12" } }, + "node_modules/yerror": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/yerror/-/yerror-6.1.1.tgz", + "integrity": "sha512-JG3e07MWkee2KolYTm/ssO0m9dSKfBuBRKRwRacqBDotPw9joUBITdN6g1D3UVFHqDQAZk6EGV52pCoPFDaHfg==", + "engines": { + "node": ">=12.19.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -5510,6 +5531,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@sciactive/splitn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sciactive/splitn/-/splitn-1.0.0.tgz", + "integrity": "sha512-z3CI0SVzAsLYmRvLDS1pOzIX9ztCVXIT9n4MMRGM7GVbu8clZqbXoz3/NytQfyP2KSeYU7YoZYlQpYkQ7KsQ1w==" + }, "@sinclair/typebox": { "version": "0.24.27", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.27.tgz", @@ -5581,15 +5607,6 @@ "@babel/types": "^7.3.0" } }, - "@types/basic-auth": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/basic-auth/-/basic-auth-1.1.3.tgz", - "integrity": "sha512-W3rv6J0IGlxqgE2eQ2pTb0gBjaGtejQpJ6uaCjz3UQ65+TFTPC5/lAE+POfx1YLdjtxvejJzsIAfd3MxWiVmfg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -5721,6 +5738,12 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "@types/yargs": { "version": "17.0.11", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.11.tgz", @@ -5871,14 +5894,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "requires": { - "safe-buffer": "5.1.2" - } - }, "body-parser": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", @@ -6536,6 +6551,15 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-auth-utils-hperrin": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/http-auth-utils-hperrin/-/http-auth-utils-hperrin-3.0.4.tgz", + "integrity": "sha512-gRV9GMNpT4UTLbCdE4Y+PCjK1vR7tULbKAwJmUGvXsfaCIHMPFVC3HBJSpSGyFxTNJrFxZbc4eK1tFW9zqZznQ==", + "requires": { + "@sciactive/splitn": "^1.0.0", + "yerror": "^6.0.2" + } + }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -7886,7 +7910,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -8226,6 +8251,11 @@ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, "v8-to-istanbul": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", @@ -8321,6 +8351,11 @@ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true }, + "yerror": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/yerror/-/yerror-6.1.1.tgz", + "integrity": "sha512-JG3e07MWkee2KolYTm/ssO0m9dSKfBuBRKRwRacqBDotPw9joUBITdN6g1D3UVFHqDQAZk6EGV52pCoPFDaHfg==" + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/packages/authenticator-custom/package.json b/packages/authenticator-custom/package.json index 64ae18d..b4ca959 100644 --- a/packages/authenticator-custom/package.json +++ b/packages/authenticator-custom/package.json @@ -34,17 +34,18 @@ "homepage": "https://github.com/sciactive/nephele#readme", "devDependencies": { "@tsconfig/recommended": "^1.0.1", - "@types/basic-auth": "^1.1.3", "@types/express": "^4.17.13", "@types/jest": "^27.5.0", + "@types/uuid": "^8.3.4", "express": "^4.18.1", "jest": "^28.1.0", "ts-jest": "^28.0.2", "typescript": "^4.6.4" }, "dependencies": { - "basic-auth": "^2.0.1", - "nephele": "^1.0.0-alpha.9" + "http-auth-utils-hperrin": "^3.0.4", + "nephele": "^1.0.0-alpha.9", + "uuid": "^8.3.2" }, "engines": { "node": ">=16" diff --git a/packages/authenticator-custom/src/Authenticator.ts b/packages/authenticator-custom/src/Authenticator.ts index 07b32a4..99da9a5 100644 --- a/packages/authenticator-custom/src/Authenticator.ts +++ b/packages/authenticator-custom/src/Authenticator.ts @@ -1,5 +1,11 @@ +import crypto from 'node:crypto'; import type { Request } from 'express'; -import basicAuth from 'basic-auth'; +import { + parseAuthorizationHeader, + BASIC, + DIGEST, +} from 'http-auth-utils-hperrin'; +import { v4 as uuid } from 'uuid'; import type { Authenticator as AuthenticatorInterface, AuthResponse as NepheleAuthResponse, @@ -10,10 +16,11 @@ import User from './User.js'; export type AuthenticatorConfig = { /** - * A function that takes a username and password and returns a promise that - * resolves to a user if the authentication succeeds, or null otherwise. + * A function that takes a username and returns a promise that resolves to a + * user if the user exists or it's not possible to tell whether they exist, or + * null otherwise. */ - auth: (username: string, password: string) => Promise; + getUser: (username: string) => Promise; /** * The realm is the name reported by the server when the user is prompted to * authenticate. @@ -22,57 +29,291 @@ export type AuthenticatorConfig = { * semicolon). */ realm?: string; -}; + /** + * A private key used to calculate nonce values for Digest authentication. + * + * If you do not provide one, one will be generated, but this does mean that + * with Digest authentication, clients will only be able to authenticate to + * _that_ particular server. If you have multiple servers or multiple + * instances of Nephele that serve the same source data, you should provide + * the same key to all of them in order to use Digest authentication + * correctly. + */ + key?: string; + /** + * The number of milliseconds for which a nonce is valid once issued. Defaults + * to 6 hours. + */ + nonceTimeout?: number; +} & ( + | { + /** + * Authorize a User returned by `getUser` with a password. + * + * The returned promise should resolve to true if the user is successfully + * authenticated, false otherwise. + * + * The Basic mechanism requires the user to submit their username and + * password in plain text with the request, so only use this if the + * connection is secured through some means like TLS. If you provide + * `authBasic`, the server will advertise support for the Basic mechanism. + */ + authBasic: (user: User, password: string) => Promise; + } + | { + /** + * Retrieve a User's password or hash for Digest authentication. + * + * The returned promise should resolve to the password or hash if the user + * exists, or null otherwise. If the password is returned, it will be + * hashed, however, you can also return a prehashed string of + * SHA256(username:realm:password) or MD5(username:realm:password), + * depending on the requested algorithm. + * + * The Digest mechansism requires the user to cryptographically hash their + * password with the request, so it will not divulge their password to + * eaves droppers. However, it is still less safe than using TLS and Basic + * authentication. If you provide `authDigest`, the server will advertise + * support for the Digest mechanism. + */ + authDigest: ( + user: User, + realm: string, + algorithm: 'sha256' | 'md5' + ) => Promise<{ password: string } | { hash: string } | null>; + } +); export type AuthResponse = NepheleAuthResponse; +class StaleUnauthorizedError extends UnauthorizedError {} + +function hash(input: string, algorithm: 'md5' | 'sha256') { + return crypto.createHash(algorithm).update(input).digest('hex').toLowerCase(); +} + /** * Nephele custom authenticator. */ export default class Authenticator implements AuthenticatorInterface { - auth: (username: string, password: string) => Promise; + getUser: (username: string) => Promise; + authBasic?: (user: User, password: string) => Promise; + authDigest?: ( + user: User, + realm: string, + algorithm: 'sha256' | 'md5' + ) => Promise<{ password: string } | { hash: string } | null>; realm: string; + key: string; + nonceTimeout: number; - constructor({ auth, realm = 'Nephele WebDAV Service' }: AuthenticatorConfig) { - this.auth = auth; - this.realm = realm; + constructor(config: AuthenticatorConfig) { + 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.key = config.key || uuid(); + // Nonce is valid for 6 hours by default. + this.nonceTimeout = config.nonceTimeout || 1000 * 60 * 60 * 6; } async authenticate(request: Request, response: AuthResponse) { const authorization = request.get('Authorization'); - let username = ''; - let password = ''; try { if (authorization) { - const auth = basicAuth.parse(authorization); - if (auth) { - username = auth.name; - password = auth.pass; - } - } + const auth = parseAuthorizationHeader(authorization); - if (username === '' && password === '') { - throw new UnauthorizedError( - 'You must authenticate to access this server.' - ); - } + if (auth.type === 'Basic' && 'password' in auth.data) { + let { username, password } = auth.data; + + if (this.authBasic == null || (!username && !password)) { + throw new UnauthorizedError( + 'You must authenticate to access this server.' + ); + } + + const user = await this.getUser(username); - const user = await this.auth(username, password); + if (user == null) { + throw new UnauthorizedError( + 'The provided credentials are not correct.' + ); + } - if (user == null) { - throw new UnauthorizedError( - 'The credentials you provided were 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 || + !username || + opaque == null || + realm !== this.realm || + (uri !== request.url && + uri !== + `${request.protocol}://${request.headers.host}${request.url}`) + ) { + throw new UnauthorizedError( + 'You must authenticate to access this server.' + ); + } + + 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: string; + + 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: string; + 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; + } } - return user; + throw new UnauthorizedError( + 'You must authenticate to access this server.' + ); } catch (e: any) { if (e instanceof UnauthorizedError) { - response.set( - 'WWW-Authenticate', - `Basic realm="${this.realm}", charset="UTF-8"` - ); + const auths: string[] = []; + 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' as 'MD5-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); } throw e; } diff --git a/testserver.ts b/testserver.ts index 55b03bd..f1ce8f8 100644 --- a/testserver.ts +++ b/testserver.ts @@ -52,12 +52,18 @@ app.use( }), authenticator: userpassdefined ? new CustomAuthenticator({ - auth: async (username, password) => { - if (username === envuser && password === envpass) { + getUser: async (username) => { + if (username === envuser) { return new CustomUser({ username }); } return null; }, + authBasic: async (user, password) => { + if (user.username === envuser && password === envpass) { + return true; + } + return false; + }, }) : pam ? new PamAuthenticator()