0
Fork 0
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:
Alex Kocharin 2014-11-16 15:37:50 +03:00
parent 322f64d517
commit 2f541130ab
5 changed files with 136 additions and 34 deletions

View file

@ -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,
}
}

View file

@ -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),
})
})
}
})

View file

@ -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) {

View file

@ -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')) {

View file

@ -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'