mirror of
https://github.com/verdaccio/verdaccio.git
synced 2025-03-25 02:32:52 -05:00
auth tokens draft
This commit is contained in:
parent
322f64d517
commit
2f541130ab
5 changed files with 136 additions and 34 deletions
129
lib/auth.js
129
lib/auth.js
|
@ -1,5 +1,6 @@
|
|||
var assert = require('assert')
|
||||
var Crypto = require('crypto')
|
||||
var jju = require('jju')
|
||||
var Error = require('http-errors')
|
||||
var Path = require('path')
|
||||
var Logger = require('./logger')
|
||||
|
@ -10,6 +11,7 @@ function Auth(config) {
|
|||
var self = Object.create(Auth.prototype)
|
||||
self.config = config
|
||||
self.logger = Logger.logger.child({ sub: 'auth' })
|
||||
self.secret = config.secret || Crypto.pseudoRandomBytes(32)
|
||||
|
||||
var stuff = {
|
||||
config: config,
|
||||
|
@ -92,13 +94,16 @@ Auth.prototype.authenticate = function(user, password, cb) {
|
|||
;(function next() {
|
||||
var p = plugins.shift()
|
||||
p.authenticate(user, password, function(err, groups) {
|
||||
if (err || groups) return cb(err, groups)
|
||||
if (err) return cb(err)
|
||||
if (groups != null && groups != false)
|
||||
return cb(err, AuthenticatedUser(user, groups))
|
||||
next()
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
Auth.prototype.add_user = function(user, password, cb) {
|
||||
var self = this
|
||||
var plugins = this.plugins.slice(0)
|
||||
|
||||
;(function next() {
|
||||
|
@ -111,14 +116,15 @@ Auth.prototype.add_user = function(user, password, cb) {
|
|||
next()
|
||||
} else {
|
||||
p[n](user, password, function(err, ok) {
|
||||
if (err || ok) return cb(err, ok)
|
||||
if (err) return cb(err)
|
||||
if (ok) return self.authenticate(user, password, cb)
|
||||
next()
|
||||
})
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
Auth.prototype.auth_middleware = function() {
|
||||
Auth.prototype.basic_middleware = function() {
|
||||
var self = this
|
||||
return function(req, res, _next) {
|
||||
req.pause()
|
||||
|
@ -150,14 +156,14 @@ Auth.prototype.auth_middleware = function() {
|
|||
var index = credentials.indexOf(':')
|
||||
|
||||
if (scheme !== 'Basic' || index < 0)
|
||||
return next( Error[400]('bad authorization header') )
|
||||
return next()
|
||||
|
||||
var user = credentials.slice(0, index)
|
||||
var pass = credentials.slice(index + 1)
|
||||
|
||||
self.authenticate(user, pass, function(err, groups) {
|
||||
if (!err && groups != null && groups != false) {
|
||||
req.remote_user = AuthenticatedUser(user, groups)
|
||||
self.authenticate(user, pass, function(err, user) {
|
||||
if (!err) {
|
||||
req.remote_user = user
|
||||
next()
|
||||
} else {
|
||||
req.remote_user = AnonymousUser()
|
||||
|
@ -167,9 +173,47 @@ Auth.prototype.auth_middleware = function() {
|
|||
}
|
||||
}
|
||||
|
||||
Auth.prototype.bearer_middleware = function() {
|
||||
var self = this
|
||||
return function(req, res, _next) {
|
||||
req.pause()
|
||||
function next(err) {
|
||||
req.resume()
|
||||
return _next.apply(null, arguments)
|
||||
}
|
||||
|
||||
if (req.remote_user != null && req.remote_user.name !== undefined)
|
||||
return next()
|
||||
req.remote_user = AnonymousUser()
|
||||
|
||||
var authorization = req.headers.authorization
|
||||
if (authorization == null) return next()
|
||||
|
||||
var parts = authorization.split(' ')
|
||||
|
||||
if (parts.length !== 2)
|
||||
return next( Error[400]('bad authorization header') )
|
||||
|
||||
var scheme = parts[0]
|
||||
var token = parts[1]
|
||||
|
||||
if (scheme !== 'Bearer')
|
||||
return next()
|
||||
|
||||
try {
|
||||
var user = self.decode_token(token)
|
||||
} catch(err) {
|
||||
return next(err)
|
||||
}
|
||||
|
||||
req.remote_user = AuthenticatedUser(user.u, user.g)
|
||||
req.remote_user.token = token
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
Auth.prototype.cookie_middleware = function() {
|
||||
var self = this
|
||||
|
||||
return function(req, res, _next) {
|
||||
req.pause()
|
||||
function next(err) {
|
||||
|
@ -182,40 +226,71 @@ Auth.prototype.cookie_middleware = function() {
|
|||
|
||||
req.remote_user = AnonymousUser()
|
||||
|
||||
var cookie = req.cookies.get('token')
|
||||
if (cookie == null) return next()
|
||||
var token = req.cookies.get('token')
|
||||
if (token == null) return next()
|
||||
|
||||
var credentials = Buffer(cookie, 'base64').toString()
|
||||
var index = credentials.indexOf(':')
|
||||
try {
|
||||
var user = self.decode_token(token, 60*60 /* 1 hour */)
|
||||
} catch(err) {
|
||||
return next()
|
||||
}
|
||||
|
||||
var user = credentials.slice(0, index)
|
||||
var pass = credentials.slice(index + 1)
|
||||
|
||||
self.authenticate(user, pass, function(err, groups) {
|
||||
if (!err && groups != null && groups != false) {
|
||||
req.remote_user = AuthenticatedUser(user, groups)
|
||||
next()
|
||||
} else {
|
||||
req.remote_user = AnonymousUser()
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
req.remote_user = AuthenticatedUser(user.u, user.g)
|
||||
req.remote_user.token = token
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
Auth.prototype.issue_token = function(user) {
|
||||
var data = jju.stringify({
|
||||
u: user.name,
|
||||
g: user.real_groups && user.real_groups.length ? user.real_groups : undefined,
|
||||
t: ~~(Date.now()/1000),
|
||||
}, { indent: false })
|
||||
|
||||
data = Buffer(data, 'utf8')
|
||||
var mac = Crypto.createHmac('sha256', this.secret).update(data).digest()
|
||||
return Buffer.concat([ data, mac ]).toString('base64')
|
||||
}
|
||||
|
||||
Auth.prototype.decode_token = function(str, expire_time) {
|
||||
var buf = Buffer(str, 'base64')
|
||||
if (buf.length <= 32) throw Error[401]('invalid token')
|
||||
|
||||
var data = buf.slice(0, buf.length - 32)
|
||||
var their_mac = buf.slice(buf.length - 32)
|
||||
var good_mac = Crypto.createHmac('sha256', this.secret).update(data).digest()
|
||||
|
||||
their_mac = Crypto.createHash('sha512').update(their_mac).digest('hex')
|
||||
good_mac = Crypto.createHash('sha512').update(good_mac).digest('hex')
|
||||
if (their_mac !== good_mac) throw Error[401]('bad signature')
|
||||
|
||||
// make token expire in 24 hours
|
||||
// TODO: make configurable?
|
||||
expire_time = expire_time || 24*60*60
|
||||
|
||||
data = jju.parse(data.toString('utf8'))
|
||||
if (Math.abs(data.t - ~~(Date.now()/1000)) > expire_time)
|
||||
throw Error[401]('token expired')
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
function AnonymousUser() {
|
||||
return {
|
||||
name: undefined,
|
||||
// groups without '@' are going to be deprecated eventually
|
||||
// groups without '$' are going to be deprecated eventually
|
||||
groups: [ '$all', '$anonymous', '@all', '@anonymous', 'all', 'undefined', 'anonymous' ],
|
||||
real_groups: [],
|
||||
}
|
||||
}
|
||||
|
||||
function AuthenticatedUser(name, groups) {
|
||||
groups = groups.concat([ '$all', '$authenticated', '@all', '@authenticated', 'all' ])
|
||||
var _groups = (groups || []).concat([ '$all', '$authenticated', '@all', '@authenticated', 'all' ])
|
||||
return {
|
||||
name: name,
|
||||
groups: groups,
|
||||
groups: _groups,
|
||||
real_groups: groups,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,10 +26,20 @@ module.exports = function(config, auth, storage) {
|
|||
app.param('org_couchdb_user', match(/^org\.couchdb\.user:/))
|
||||
app.param('anything', match(/.*/))
|
||||
|
||||
app.use(auth.auth_middleware())
|
||||
app.use(auth.basic_middleware())
|
||||
app.use(auth.bearer_middleware())
|
||||
app.use(expressJson5({ strict: false, limit: config.max_body_size || '10mb' }))
|
||||
app.use(Middleware.anti_loop(config))
|
||||
|
||||
// for "npm whoami"
|
||||
app.get('/whoami', function(req, res, next) {
|
||||
if (req.headers.referer === 'whoami') {
|
||||
next({ username: req.remote_user.name })
|
||||
} else {
|
||||
next('route')
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: anonymous user?
|
||||
app.get('/:package/:version?', can('access'), function(req, res, next) {
|
||||
storage.get_package(req.params.package, { req: req }, function(err, info) {
|
||||
|
@ -104,7 +114,8 @@ module.exports = function(config, auth, storage) {
|
|||
if (req.remote_user.name != null) {
|
||||
res.status(201)
|
||||
return next({
|
||||
ok: 'you are authenticated as "' + req.remote_user.name + '"',
|
||||
ok: "you are authenticated as '" + req.remote_user.name + "'",
|
||||
token: auth.issue_token(req.remote_user),
|
||||
})
|
||||
} else {
|
||||
if (typeof(req.body.name) !== 'string' || typeof(req.body.password) !== 'string') {
|
||||
|
@ -114,7 +125,7 @@ module.exports = function(config, auth, storage) {
|
|||
return next( Error[422]('user/password is not found in request (npm issue?)') )
|
||||
}
|
||||
}
|
||||
auth.add_user(req.body.name, req.body.password, function(err) {
|
||||
auth.add_user(req.body.name, req.body.password, function(err, user) {
|
||||
if (err) {
|
||||
if (err.status < 500 && err.message === 'this user already exists') {
|
||||
// with npm registering is the same as logging in
|
||||
|
@ -124,8 +135,12 @@ module.exports = function(config, auth, storage) {
|
|||
return next(err)
|
||||
}
|
||||
|
||||
req.remote_user = user
|
||||
res.status(201)
|
||||
return next({ ok: 'user "' + req.body.name + '" created' })
|
||||
return next({
|
||||
ok: "user '" + req.body.name + "' created",
|
||||
token: auth.issue_token(req.remote_user),
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -76,9 +76,15 @@ module.exports = function(config, auth, storage) {
|
|||
})
|
||||
|
||||
app.post('/-/login', function(req, res, next) {
|
||||
var base = config.url_prefix || req.protocol + '://' + req.get('host')
|
||||
res.cookies.set('token', Buffer(req.body.user + ':' + req.body.pass).toString('base64'))
|
||||
res.redirect(base)
|
||||
auth.authenticate(req.body.user, req.body.pass, function(err, user) {
|
||||
if (!err) {
|
||||
req.remote_user = user
|
||||
res.cookies.set('token', auth.issue_token(req.remote_user))
|
||||
}
|
||||
|
||||
var base = config.url_prefix || req.protocol + '://' + req.get('host')
|
||||
res.redirect(base)
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/-/logout', function(req, res, next) {
|
||||
|
|
|
@ -88,6 +88,11 @@ module.exports.allow = function(config) {
|
|||
}
|
||||
|
||||
module.exports.final = function(body, req, res, next) {
|
||||
if (res.statusCode === 401 && !res.getHeader('WWW-Authenticate')) {
|
||||
// they say it's required for 401, so...
|
||||
res.header('WWW-Authenticate', 'Basic, Bearer')
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof(body) === 'string' || typeof(body) === 'object') {
|
||||
if (!res.getHeader('Content-type')) {
|
||||
|
|
|
@ -41,6 +41,7 @@ dependencies:
|
|||
highlight.js: '8.x'
|
||||
lunr: '>=0.5.2 <1.0.0-0'
|
||||
marked: '>=0.3.2 <1.0.0-0'
|
||||
jju: '1.x'
|
||||
|
||||
# bundled deps, see below
|
||||
mkdirp: '>=0.3.5 <1.0.0-0'
|
||||
|
|
Loading…
Add table
Reference in a new issue