Skip to content

Commit

Permalink
Allow authentication with registry.npmjs.com (#94)
Browse files Browse the repository at this point in the history
* Allow npm auth credentials to be sent to uplink

* Add tests for npm auth credentials

* Stub redis

* Fix codeclimate issue
  • Loading branch information
Ianfeather authored and dgautsch committed Jan 9, 2018
1 parent 262e563 commit d1c5aae
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 6 deletions.
4 changes: 4 additions & 0 deletions lib/config.js
Expand Up @@ -20,6 +20,10 @@ module.exports = {
bucket: env.AWS_S3_BUCKET,
region: env.AWS_DEFAULT_REGION
},
npm: {
basic: env.NPM_AUTH_BASIC,
token: env.NPM_AUTH_TOKEN
},
auth: {
write: (env.NPM_REGISTER_AUTH_WRITE || 'true') === 'true',
read: (env.NPM_REGISTER_AUTH_READ || 'false') === 'true'
Expand Down
24 changes: 18 additions & 6 deletions lib/npm.js
Expand Up @@ -5,6 +5,14 @@ const redis = require('./redis')
const warn = require('./warn')
const {cacheKey} = require('./cache')

function getAuthHeader () {
if (config.npm.basic) {
return `Basic ${config.npm.basic}`
} else if (config.npm.token) {
return `Bearer ${config.npm.token}`
}
}

async function isEtagFresh (name, etag) {
try {
let cache = await redis.get(`${cacheKey(name)}/etag`)
Expand Down Expand Up @@ -43,14 +51,20 @@ async function updateCache (pkg) {
}
}

function getRequestOpts (etag, headers = {}) {
let opts = {timeout: config.timeout, headers}
if (etag) opts.headers['if-none-match'] = etag
let authHeader = getAuthHeader()
if (authHeader) opts.headers['Authorization'] = authHeader
return opts
}

async function get (name, etag) {
try {
if (etag && redis && (await isEtagFresh(name, etag))) return 304
let pkg = redis ? await fetchFromCache(name) : null
if (pkg) return pkg
let opts = {timeout: config.timeout, headers: {}}
if (etag) opts.headers['if-none-match'] = etag
let req = await HTTP.request(url.resolve(config.uplink.href, '/' + name.replace(/\//, '%2F')), opts)
let req = await HTTP.request(url.resolve(config.uplink.href, '/' + name.replace(/\//, '%2F')), getRequestOpts(etag))
pkg = req.body
if (!pkg.versions) return 404
pkg.etag = req.response.headers.etag
Expand All @@ -71,9 +85,7 @@ async function get (name, etag) {
}

function getTarball (name, filename) {
return HTTP.stream(`${config.uplink.href}${name}/-/${filename}`, {
timeout: config.timeout
})
return HTTP.stream(`${config.uplink.href}${name}/-/${filename}`, getRequestOpts())
}

async function getLatest (name) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -22,6 +22,7 @@
"on-finished": "^2.3.0",
"opbeat": "^4.15.1",
"s3": "^4.4.0",
"sinon": "^4.1.2",
"uuid": "^3.1.0",
"yargs": "^8.0.2"
},
Expand Down
65 changes: 65 additions & 0 deletions test/packages.js
@@ -1,12 +1,15 @@
let app = require('../lib/server')
let request = require('supertest').agent(app.listen())
let user = require('../lib/user')
let redis = require('../lib/redis')
let co = require('co')
let url = require('url')
let crypto = require('crypto')
let fs = require('fs')
let config = require('../lib/config')
let expect = require('unexpected')
let sinon = require('sinon')
let http = require('http-call').HTTP

// make sure this user is in the htpasswd file
const testUser = {name: 'test', password: 'test'}
Expand Down Expand Up @@ -34,7 +37,17 @@ storageBackends.forEach(storage => {
let Storage = require('../lib/storage/' + storage)
config.storage = new Storage()
token = yield user.authenticate(testUser)
sinon.spy(http, 'request')
if (redis) sinon.stub(redis, 'zget').returns(null)
}))
after(() => {
http.request.restore()
redis.zget.restore()
})
afterEach(() => {
http.request.reset()
redis.zget.reset()
})

describe('packages', () => {
describe('GET /:package (package metadata)', () => {
Expand All @@ -53,6 +66,31 @@ storageBackends.forEach(storage => {
.use(bearer(token))
.expect(200))
})
it('can request from npm using basic auth', () => {
config.auth.read = false
config.npm.basic = 'testing basic auth'
return request.get('/mocha')
.accept('json')
.expect(200)
.then((r) => {
let requestHeaders = http.request.getCall(0).args[1].headers
expect(requestHeaders.Authorization, 'to equal', 'Basic testing basic auth')
expect(r.body.name, 'to equal', 'mocha')
})
})
it('can request from npm using an auth token', () => {
config.auth.read = false
config.npm.basic = null
config.npm.token = 'testing auth token'
return request.get('/mocha')
.accept('json')
.expect(200)
.then((r) => {
let requestHeaders = http.request.getCall(0).args[1].headers
expect(requestHeaders.Authorization, 'to equal', 'Bearer testing auth token')
expect(r.body.name, 'to equal', 'mocha')
})
})
})

describe('GET /:package/-/:filename (package tarball)', () => {
Expand Down Expand Up @@ -89,6 +127,33 @@ storageBackends.forEach(storage => {
.use(bearer(token))
.expect(302))
})
it('can request tarballs from npm using basic auth', () => {
config.auth.read = false
config.npm.basic = 'tarball basic auth'
return request.get('/mocha')
.accept('json')
.expect(200)
.then((r) => r.body.versions['1.0.0'].dist)
.then((dist) => {
request.get(url.parse(dist.tarball).path)
let requestHeaders = http.request.getCall(0).args[1].headers
expect(requestHeaders.Authorization, 'to equal', 'Basic tarball basic auth')
})
})
it('can request tarballs from npm using basic auth', () => {
config.auth.read = false
config.npm.basic = null
config.npm.token = 'tarball auth token'
return request.get('/mocha')
.accept('json')
.expect(200)
.then((r) => r.body.versions['1.0.0'].dist)
.then((dist) => {
request.get(url.parse(dist.tarball).path)
let requestHeaders = http.request.getCall(0).args[1].headers
expect(requestHeaders.Authorization, 'to equal', 'Bearer tarball auth token')
})
})
})

describe('PUT /:package (npm publish)', () => {
Expand Down

0 comments on commit d1c5aae

Please sign in to comment.