@quarks/quarks-iam
Version:
A modern authorization server built to authenticate your users and protect your APIs
542 lines (434 loc) • 13 kB
JavaScript
/**
* Module dependencies
*/
var pkg = require('../package.json')
var qs = require('qs')
var URL = require('url')
var util = require('util')
var crypto = require('crypto')
var request = require('superagent')
var map = require('modinha').map
var User = require('../models/User')
var Strategy = require('passport-strategy')
var agent = 'Anvil Connect/v' + pkg.version
var nowSeconds = require('../lib/time-utils').nowSeconds
/**
* OAuthStrategy
*/
function OAuthStrategy (provider, client, verify) {
Strategy.call(this)
this.provider = provider
this.endpoints = provider.endpoints
this.mapping = provider.mapping
this.client = client
this.verify = verify
this.name = provider.id
}
util.inherits(OAuthStrategy, Strategy)
/**
* Verifier
*/
function verifier (req, auth, userInfo, done) {
User.connect(req, auth, userInfo, function (err, user) {
if (err) { return done(err) }
done(null, user)
})
}
OAuthStrategy.verifier = verifier
/**
* Initialize
*/
function initialize (provider, configuration) {
return new OAuthStrategy(provider, configuration, verifier)
}
OAuthStrategy.initialize = initialize
/**
* Authorization Header Params
* https://tools.ietf.org/html/rfc5849#section-3.5.1
*/
function authorizationHeaderParams (data) {
var keys = Object.keys(data).sort()
var encoded = ''
keys.forEach(function (key, i) {
encoded += key
encoded += '="'
encoded += encodeOAuthData(data[key])
encoded += '"'
if (i < keys.length - 1) {
encoded += ', '
}
})
return encoded
}
OAuthStrategy.authorizationHeaderParams = authorizationHeaderParams
/**
* Encode Data
* https://tools.ietf.org/html/rfc5849#section-3.6
*/
function encodeOAuthData (data) {
// empty data
if (!data || data === '') {
return ''
// non-empty data
} else {
return encodeURIComponent(data)
.replace(/!/g, '%21')
.replace(/'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/\*/g, '%2A')
}
}
OAuthStrategy.encodeOAuthData = encodeOAuthData
/**
* Timestamp
*/
function timestamp () {
return nowSeconds()
}
OAuthStrategy.timestamp = timestamp
/**
* Nonce
*/
// function nonce (size) {
// return crypto.randomBytes(size).toString('hex').slice(0, 10)
// }
var NCHARS = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9'
]
function nonce (size) {
var res = []
var len = NCHARS.length
var pos
for (var i = 0; i < size; i++) {
pos = Math.floor(Math.random() * len)
res[i] = NCHARS[pos]
}
return res.join('')
}
OAuthStrategy.nonce = nonce
/**
* Signature Base String URI
* https://tools.ietf.org/html/rfc5849#section-3.4.1.1
* https://tools.ietf.org/html/rfc5849#section-3.4.1.2
*/
function signatureBaseStringURI (uri) {
var url = URL.parse(uri, true)
var protocol = url.protocol
var hostname = url.hostname
var pathname = url.pathname
var port = ''
var result = ''
if (url.port) {
if ((protocol === 'http:' && url.port !== '80') ||
(protocol === 'https:' && url.port !== '443')) {
port = ':' + url.port
}
}
if (!pathname || pathname === '') {
pathname = '/'
}
result += protocol
result += '//'
result += hostname
result += port
result += pathname
return result
}
OAuthStrategy.signatureBaseStringURI = signatureBaseStringURI
/**
* Normalize Parameters
* https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
*/
function normalizeParameters (data) {
var encoded = []
var normalized = ''
// Convert object into nested arrays
// and encode the keys and values.
Object.keys(data).forEach(function (key) {
encoded[encoded.length] = [
encodeOAuthData(key),
encodeOAuthData(data[key])
]
})
// Sort by keys and values
encoded.sort(function (a, b) {
return (a[0] === b[0])
? (a[1] < b[1])
? -1
: 1
: (a[0] < b[0])
? -1
: 1
})
// Encode parameters similar to
// query string.
encoded.forEach(function (pair, i) {
normalized += pair[0]
normalized += '='
normalized += pair[1]
if (i < encoded.length - 1) {
normalized += '&'
}
})
// bada boom
return normalized
}
OAuthStrategy.normalizeParameters = normalizeParameters
/**
* Signature Base String
* https://tools.ietf.org/html/rfc5849#section-3.4.1
*/
function signatureBaseString (method, url, parameters) {
return method.toUpperCase() + '&' +
encodeOAuthData(signatureBaseStringURI(url)) + '&' +
encodeOAuthData(parameters)
}
OAuthStrategy.signatureBaseString = signatureBaseString
/**
* Signature
*/
function sign (method, input, consumerSecret, tokenSecret) {
var encoding = 'base64'
var result = ''
var key = encodeOAuthData(consumerSecret) + '&' +
encodeOAuthData(tokenSecret)
switch (method) {
case 'PLAINTEXT':
result = key
break
case 'RSA-SHA1':
result = crypto.createSign(method).update(input).sign(key, encoding)
break
case 'HMAC-SHA1':
result = crypto.createHmac('sha1', key).update(input).digest(encoding)
break
default:
// ERROR
}
return result
}
OAuthStrategy.sign = sign
/**
* Temporary Credentials
* https://tools.ietf.org/html/rfc5849#section-2.1
*/
function temporaryCredentials (done) {
var strategy = this
var provider = strategy.provider
var endpoint = strategy.endpoints.credentials
var client = strategy.client
var method = endpoint.method && endpoint.method.toLowerCase()
var url = endpoint.url
var header = endpoint.header || 'Authorization'
var scheme = endpoint.scheme || 'OAuth'
var accept = endpoint.accept || 'application/x-www-form-urlencoded'
var key = client.oauth_consumer_key
var secret = client.oauth_consumer_secret
var signer = provider.oauth_signature_method || 'PLAINTEXT'
var realm = provider.realm
var callback = provider.oauth_callback
var params = {}
// Initialize request
var req = request[method || 'post'](url)
// Build request parameters
params.oauth_consumer_key = key
params.oauth_signature_method = signer
params.oauth_timestamp = timestamp()
params.oauth_nonce = nonce(32)
params.oauth_callback = callback
params.oauth_version = '1.0'
// Generate signature (is next line needed for PLAINTEXT?)
var input = signatureBaseString(method, url, normalizeParameters(params))
params.oauth_signature = sign(signer, input, secret)
if (realm) { params.realm = realm }
// Set Authorization header
req.set(header, scheme + ' ' + authorizationHeaderParams(params))
// Set other headers
req.set('Accept', accept)
req.set('User-Agent', agent)
// Execute request
return req.end(function (err, res) {
if (err) { return done(err) }
var response = qs.parse(res.text)
if (res.statusCode !== 200) {
return done(new Error(res.text))
}
done(null, response)
})
}
OAuthStrategy.prototype.temporaryCredentials = temporaryCredentials
/**
* Resource Owner Authorization
* https://tools.ietf.org/html/rfc5849#section-2.2
*/
function resourceOwnerAuthorization (token) {
var strategy = this
var endpoint = strategy.endpoints.authorization
var url = endpoint.url
var param = endpoint.param || 'oauth_token'
strategy.redirect(url + '?' + param + '=' + token)
}
OAuthStrategy.prototype.resourceOwnerAuthorization = resourceOwnerAuthorization
/**
* Token Credentials
* https://tools.ietf.org/html/rfc5849#section-2.3
*/
function tokenCredentials (authorization, secret, done) {
var strategy = this
var provider = strategy.provider
var endpoint = strategy.endpoints.token
var client = strategy.client
var method = endpoint.method && endpoint.method.toLowerCase()
var url = endpoint.url
var header = endpoint.header || 'Authorization'
var scheme = endpoint.scheme || 'OAuth'
var accept = endpoint.accept || 'application/x-www-form-urlencoded'
var key = client.oauth_consumer_key
var signer = provider.oauth_signature_method || 'PLAINTEXT'
var verifier = authorization.oauth_verifier || null
var token = authorization.oauth_token
var params = {}
// Initialize request
var req = request[method || 'post'](url)
// Build request parameters
params.oauth_consumer_key = key
params.oauth_signature_method = signer
params.oauth_timestamp = timestamp()
params.oauth_nonce = nonce(32)
params.oauth_token = token
params.oauth_verifier = verifier
params.oauth_version = '1.0'
var input = signatureBaseString(method, url, normalizeParameters(params))
params.oauth_signature = sign(signer, input, secret)
// Set Authorization header
req.set(header, scheme + ' ' + authorizationHeaderParams(params))
// Set other headers
req.set('Accept', accept)
req.set('User-Agent', agent)
// Execute request
return req.end(function (err, res) {
if (err) { return done(err) }
var response = qs.parse(res.text)
if (res.statusCode !== 200) {
return done(new Error("Couldn't obtain token credentials"))
}
done(null, response)
})
}
OAuthStrategy.prototype.tokenCredentials = tokenCredentials
/**
* User Info
*/
function userInfo (credentials, done) {
var strategy = this
var provider = strategy.provider
var endpoint = strategy.endpoints.user
var mapping = strategy.mapping
var client = strategy.client
var method = endpoint.method && endpoint.method.toLowerCase()
var url = endpoint.url
var header = endpoint.header || 'Authorization'
var scheme = endpoint.scheme || 'OAuth'
var key = client.oauth_consumer_key
var token = credentials.oauth_token
var secret = client.oauth_consumer_secret
var signer = provider.oauth_signature_method || 'PLAINTEXT'
var ctype = 'application/x-www-form-urlencoded'
var params = {}
var query = { user_id: credentials.user_id } // twitter specific
// Initialize request
var req = request[method || 'get'](url)
req.query(query)
// Params
params.oauth_consumer_key = key
params.oauth_signature_method = signer
params.oauth_timestamp = timestamp()
params.oauth_nonce = nonce(32)
params.oauth_token = token
params.oauth_version = '1.0'
// Authenticate
var input = signatureBaseString(
method,
url,
normalizeParameters(params) + '&' +
normalizeParameters(query)
)
params.oauth_signature = sign(
signer, input, secret, credentials.oauth_token_secret
)
// Set Authorization header
req.set(header, scheme + ' ' + authorizationHeaderParams(params))
// Set other headers
req.set('Content-Type', ctype)
req.set('User-Agent', agent)
return req.end(function (err, res) {
if (err) { return done(err) }
// error
if (res.statusCode !== 200) {
return done(
new Error('Unable to obtain user profile.')
)
}
var profile = { provider: strategy.name }
map(mapping, res.body, profile)
done(null, profile)
})
}
OAuthStrategy.prototype.userInfo = userInfo
/**
* Authenticate
*/
function authenticate (req, options) {
var strategy = this
// Handle the authorization response
if (req.query && req.query.oauth_token) {
if (!req.session['oauth']) {
strategy.error(
new Error('Failed to find request token in session')
)
}
var authorization = req.query
var secret = req.session['oauth'].oauth_token_secret
// request token credentials
strategy.tokenCredentials(authorization, secret, function (err, credentials) {
if (err) { return strategy.error(err) }
// clean up session
delete req.session['oauth']
// request user info
strategy.userInfo(credentials, function (err, profile) {
if (err) { return strategy.error(err) }
// invoke the callback
strategy.verify(req, credentials, profile, function (err, user, info) {
if (err) { return strategy.error(err) }
if (!user) { return strategy.fail(info) }
strategy.success(user, info)
})
})
})
// Initiate the authorization flow
} else {
strategy.temporaryCredentials(function (err, response) {
if (err || (!response && response.oauth_token)) {
return strategy.error(
new Error('Failed to obtain OAuth request token')
)
}
// Store response in session
if (!req.session['oauth']) { req.session['oauth'] = {} }
req.session['oauth'].oauth_token = response.oauth_token
req.session['oauth'].oauth_token_secret = response.oauth_token_secret
// Redirect to OAuth server
strategy.resourceOwnerAuthorization(response.oauth_token)
})
}
}
OAuthStrategy.prototype.authenticate = authenticate
/**
* Exports
*/
module.exports = OAuthStrategy