couchdb-nano icon indicating copy to clipboard operation
couchdb-nano copied to clipboard

Nano does not work when using special characters in CouchDB password

Open matthiasunt opened this issue 5 years ago • 7 comments

Nano does not parse a CouchDB host, username, and password correctly if the string which is passed to the constructor contains special characters.

Steps to Reproduce (for bugs)

import * as Nano  from 'nano'

let n = Nano('http://admin:g[?6asdwrF@localhost:5984');
console.log(n);
let db = n.db.use('people');

Your Environment

  • Version used: 8.1.0
  • Operating System and version (desktop or mobile): macOs 10.14.6

matthiasunt avatar Aug 07 '19 09:08 matthiasunt

You're a lifesaver. Thank you for pointing this out!

smilingkylan avatar May 01 '20 07:05 smilingkylan

Hi,

Perhaps the user name and password could be passed as Header in a HTTP POST (GET to?) command, something like:

$.ajax({ type: 'POST', url: http://theappurl.com/api/v1/method/, data: {}, crossDomain: true, beforeSend: function(xhr) { xhr.setRequestHeader('Authorization', 'Basic ' + btoa(unescape(encodeURIComponent(YOUR_USERNAME + ':' + YOUR_PASSWORD)))) }});

Source: https://stackoverflow.com/questions/18264601/how-to-send-a-correct-authorization-header-for-basic-authentication

Source: https://developer.mozilla.org/en-US/docs/Glossary/Base64

Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs

On Wed, 7 Aug 2019 at 11:42, Matthias [email protected] wrote:

Nano does not parse a CouchDB host, username, and password correctly if the password which is passed to the constructor contains special characters. Steps to Reproduce (for bugs)

import * as Nano from 'nano'

let n = Nano('http://admin:g[?6asdwrF@localhost:5984'); console.log(n); let db = n.db.use('people');

Your Environment

  • Version used: 8.1.0
  • Operating System and version (desktop or mobile): macOs 10.14.6

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/apache/couchdb-nano/issues/174?email_source=notifications&email_token=ABKTZUWO5EK4TV77NTVUW3TQDKKJDA5CNFSM4IJ6N732YY3PNVWWK3TUL52HS4DFUVEXG43VMWVGG33NNVSW45C7NFSM4HD26LTA, or mute the thread https://github.com/notifications/unsubscribe-auth/ABKTZUS5I3SEBU5QANEC5MDQDKKJDANCNFSM4IJ6N73Q .

SinanGabel avatar May 01 '20 07:05 SinanGabel

Have you tried using encodeURI on the password?

let n = Nano('http://admin:' + encodeURI('g[?6asdwrF') + '@localhost:5984');

I suspect the problem is that the URL you used is not valid. From REPL on node.js 14:

> new URL('http://admin:g[?6asdwrF@localhost:5984')
Uncaught:
TypeError [ERR_INVALID_URL]: Invalid URL: http://admin:g[?6asdwrF@localhost:5984
    at onParseError (internal/url.js:257:9)
    at new URL (internal/url.js:333:5)
    at repl:1:1
    at Script.runInThisContext (vm.js:131:20)
    at REPLServer.defaultEval (repl.js:436:29)
    at bound (domain.js:429:14)
    at REPLServer.runBound [as eval] (domain.js:442:12)
    at REPLServer.onLine (repl.js:763:10)
    at REPLServer.emit (events.js:327:22)
    at REPLServer.EventEmitter.emit (domain.js:485:12) {
  input: 'http://admin:g[?6asdwrF@localhost:5984',
  code: 'ERR_INVALID_URL'
}

coreyfarrell avatar May 12 '20 14:05 coreyfarrell

For NodeJS developer trying to use the _changes feed, this is how I did it (you can also use request instead of get by simply replacing get with request like so: http.request({ ... })):

http.get({ 
    host: '127.0.0.1',
    port: 5984,
    path: '/books/_changes?feed=continuous',
    auth: 'user:p@ssword#'
 }, resp => {
    resp.on('data', chunk => {
        console.log('' + chunk);
    });
}).on('error', err => console.log(err));

meSmashsta avatar Mar 29 '21 14:03 meSmashsta

I've solved using

const opts = {
  url: `http://couchdb:5984`,
  
  requestDefaults: { 
    headers: {
      Authorization:"Basic " + new Buffer("admin:"+cdbpass).toString('base64')
    }
  }
}

const couchdb = nano(opts)

sl45sms avatar Nov 23 '21 08:11 sl45sms

What also works with nano>=9 is using cookie auth:

couchServer = nano({
  url: config.dbServer.protocol + config.dbServer.host,
  parseUrl: false,
  requestDefaults: {
    jar: true
  }
});
await couchServer.auth(config.dbServer.user, config.dbServer.password);

fynnlyte avatar Jan 24 '22 09:01 fynnlyte

Note: Adding the Auth header manually like this is not a good idea in nano 9:

requestDefaults: { 
    headers: {
      Authorization:"Basic " + new Buffer("admin:"+cdbpass).toString('base64')
    }
  }

Because this will cause nano to log passwords on errors, see this issue. This commit shows how passwords are redacted.

So if you don't want to use cookie auth (which requires to await nano.auth(..)), this is a good way to go with special characters inside the password:

const opts = {
  url: `http://couchdb:5984`,
  
  requestDefaults: { 
    headers: {
      auth: {
          username: "admin",
          password: "/Pa$$|w0rd^",
        },
    }
  }
}

const couchdb = nano(opts)

fynnlyte avatar Jan 25 '22 14:01 fynnlyte

Hm, for me, only the approach suggested by sl45sms worked:

const nano = nanoClient({
    url: `http://127.0.0.1:5984`,
    requestDefaults: {
        headers: {
            // works, but insecure: https://github.com/apache/couchdb-nano/issues/174#issuecomment-1021215664
            Authorization: "Basic " + new Buffer(`${config.storage.couchdb.user}:${config.storage.couchdb.password}`).toString('base64'),
        },
    },
})
nano.use('test_polling_storage').list().then(console.log)

while these 2 give Error: You are not authorized to access this db.:

const nano = nanoClient({
    url: `http://127.0.0.1:5984`,
    requestDefaults: {
        headers: {
            auth: {
                username: config.storage.couchdb.user,
                password: config.storage.couchdb.password,
            },
        },
    },
})
nano.use('test_polling_storage').list().then(console.log)

and

const nano = nanoClient({
    url: `http://127.0.0.1:5984`,
    requestDefaults: {
        jar: true,
    },
})

nano.auth(config.storage.couchdb.user, config.storage.couchdb.password)
    .then(() => nano.use('test_polling_storage').list().then(console.log))

@LyteFM any ideas why?

I have

  • nano v10.1.0
  • CouchDB v3.2.2

PS if I do, for instance,

nano.auth(config.storage.couchdb.user, encodeURIComponent(config.storage.couchdb.password))
    .then(() => nano.use('test_polling_storage').list().then(console.log))

I'm getting another error: Error: Name or password is incorrect.

YakovL avatar Jan 10 '23 11:01 YakovL

This is a problem because Node.js's URL parser doesn't like "special characters" in the password:

i.e.

const { URL } = require('url')
const u = new URL('http://admin:g[?6asdwrF@localhost:5984')
Uncaught TypeError [ERR_INVALID_URL]: Invalid URL
    at __node_internal_captureLargerStackTrace (node:internal/errors:484:5)
    at new NodeError (node:internal/errors:393:5)
    at URL.onParseError (node:internal/url:565:9)
    at new URL (node:internal/url:645:5) {
  input: 'http://admin:g[?6asdwrF@localhost:5984',
  code: 'ERR_INVALID_URL'
}

a work around, as @LyteFM points out is to provide the credentials separately:

const nano = Nano('http://127.0.0.1:5984)
await nano.auth('admin', '[!5gdg@&!')

I'd be curious to know what the "URL spec" (if there is such a thing) says about putting passwords in URLs. I'm happy to fix this in Nano if there's a standard way of approaching this.

glynnbird avatar Jan 10 '23 13:01 glynnbird

@glynnbird unfortunately, your suggestion doesn't work for me either: like I've mentioned, I've tried

const nano = nanoClient({
    url: `http://127.0.0.1:5984`,
    requestDefaults: {
        jar: true,
    },
})

nano.auth(config.storage.couchdb.user, config.storage.couchdb.password)
    .then(() => nano.use('test_polling_storage').list().then(console.log))

already and looking at your suggestion, I've also tried

const nano = nanoClient(`http://127.0.0.1:5984`)
nano.auth(config.storage.couchdb.user, config.storage.couchdb.password)
    .then(() => nano.use('test_polling_storage').list().then(console.log))

but I'm still getting Error: You are not authorized to access this db.

Just to be clear, I'm using TypeScript and nanoClient is from import nanoClient from 'nano'.

As for the "URL spec", it's actually URI, and according to wiki schemes list, those are RFC 1738, RFC 2616 (makes RFC 2068 obsolete), RFC 7230 for http: and RFC 2817, RFC 7230 for https: Looks like RFC 3986 is quite definitive (see TOC) which basically sais that..

Use of the format "user:password" in the userinfo field is deprecated

!

So, while it was a standart way to %-encode both username and password, I'm not sure what are the deprecation reasons and whether this is the way to go.

Still, I'm getting an error for what you have suggested, so I wonder what may be the problem.

YakovL avatar Jan 11 '23 09:01 YakovL

As of Nano 10.1.1, you should be able to do:

nano.auth(config.storage.couchdb.user, config.storage.couchdb.password)
    .then(() => nano.use('test_polling_storage').list().then(console.log))

i.e. emit username/password from the URL and use separate nano.auth step.

I agree with your assessment that passing credentials in a URL is deprecated. Newer versions of Nano should collect credentials separately.

glynnbird avatar Jan 11 '23 12:01 glynnbird

Awesome, after updating nano 10.1.0 → 10.1.1 this worked indeed. Thanks!

PS I've created a follow up PR, but only changed one bit in readme.md – others are to be found and updated.

YakovL avatar Jan 12 '23 08:01 YakovL

@glynnbird is the method you suggested supposed to be used regularly, or nano is supposed to keep the auth alive? In my case, I'm experiencing a problem where after a successful auth at some point I'm starting to get Error: You are not authorized to access this db. Here's the full error:

at responseHandler (<project folder>\node_modules\nano\lib\nano.js:193:20)
    at <project folder>\node_modules\nano\lib\nano.js:442:13
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {   
  scope: 'couch',
  statusCode: 401,
  request: {
    method: 'post',
    headers: {
      'content-type': 'application/json',
      accept: 'application/json',
      'user-agent': 'nano/10.1.1 (Node.js v16.13.1)',
      'Accept-Encoding': 'deflate, gzip'
    },
    qsStringifyOptions: { arrayFormat: 'repeat' },
    url: 'http://127.0.0.1:5984/test_polling_storage/_find',
    params: undefined,
    paramsSerializer: { serialize: [Function: serialize] },
    data: '{"selector":{},"limit":100,"sort":[{"isoDate":"desc"}]}',
    maxRedirects: 0,
    httpAgent: CookieAgent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      defaultPort: 80,
      protocol: 'http:',
      options: [Object: null prototype],
      requests: [Object: null prototype] {},
      sockets: [Object: null prototype],
      freeSockets: [Object: null prototype] {},
      keepAliveMsecs: 30000,
      keepAlive: true,
      maxSockets: 50,
      maxFreeSockets: 256,
      scheduling: 'lifo',
      maxTotalSockets: Infinity,
      totalSocketCount: 0,
      jar: [CookieJar],
      [Symbol(kCapture)]: false,
      [Symbol(cookieOptions)]: [Object]
    },
    httpsAgent: CookieAgent {
      _events: [Object: null prototype],
      _eventsCount: 2,
      _maxListeners: undefined,
      defaultPort: 443,
      protocol: 'https:',
      options: [Object: null prototype],
      requests: [Object: null prototype] {},
      sockets: [Object: null prototype] {},
      freeSockets: [Object: null prototype] {},
      keepAliveMsecs: 30000,
      keepAlive: true,
      maxSockets: 50,
      maxFreeSockets: 256,
      scheduling: 'lifo',
      maxTotalSockets: Infinity,
      totalSocketCount: 0,
      maxCachedSessions: 100,
      _sessionCache: [Object],
      jar: [CookieJar],
      [Symbol(kCapture)]: false,
      [Symbol(cookieOptions)]: [Object]
    }
  },
  headers: {
    uri: 'http://127.0.0.1:5984/test_polling_storage/_find',
    statusCode: 401,
    'cache-control': 'must-revalidate',
    connection: 'close',
    'content-type': 'application/json',
    date: 'Thu, 26 Jan 2023 07:59:45 GMT',
    'x-couch-request-id': 'd2b95b849e',
    'x-couchdb-body-time': '0'
  },
  errid: 'non_200',
  description: 'You are not authorized to access this db.',
  error: 'unauthorized',
  reason: 'You are not authorized to access this db.'
}

YakovL avatar Jan 26 '23 08:01 YakovL

Closing in favour of #324

glynnbird avatar Jan 26 '23 14:01 glynnbird