Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for v5 signatures #48

Merged
merged 10 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions example/duo_admin_policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env node

var nopt = require('nopt')
var duo_api = require('../')

var parsed = nopt({
'ikey': [String],
'skey': [String],
'host': [String]
}, [], process.argv, 2)

var requirements_met = (parsed.ikey && parsed.skey && parsed.host)

if (!requirements_met) {
console.error('Missing required option.\n')
}

if (parsed.help || !requirements_met) {
console.log(function () { /*
Usage:

duo_admin_policy.js --ikey IKEY --skey SKEY --host HOST

Example of making one Policy API call against the Duo service.

Options:

--ikey Admin API integration key (required)
--skey Corresponding secret key (required)
--host API hostname (required)
--help Print this help.
*/ }.toString().split(/\n/).slice(1, -1).join('\n'))
if (parsed.help) {
process.exit(0)
} else {
process.exit(1)
}
}

var client = new duo_api.Client(
parsed.ikey,
parsed.skey,
parsed.host,
duo_api.SIGNATURE_VERSION_5
)

let params = {
'policy_name': 'api_test_policy',
'sections': {
'screen_lock': {
'require_screen_lock': false
}
}
}

let policy_key = ''

client.jsonApiCall(
'POST',
'/admin/v2/policies',
params,
function (res) {
if (res.stat !== 'OK') {
console.error('API call returned error: ' + res.message)
process.exit(1)
}

res = res.response
policy_key = res.policy_key

console.log('res = ' + JSON.stringify(res, null, 4))

// Delete the created policy
deletePolicy(policy_key)
},
5
jaherne-duo marked this conversation as resolved.
Show resolved Hide resolved
)

// Delete policy function
function deletePolicy (policy_key) {
client.jsonApiCall(
'DELETE',
'/admin/v2/policies/' + policy_key,
{},
function (res) {
if (res.stat !== 'OK') {
console.error('API call returned error: ' + res.message)
process.exit(1)
}
res = res.response
console.log('Deleted policy: ' + policy_key)
},
5
jaherne-duo marked this conversation as resolved.
Show resolved Hide resolved
)
}
33 changes: 33 additions & 0 deletions lib/duo_sig.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ function canonParams (params) {
}

// Return a request's canonical representation as a string to sign.
// Canonicalization format is version 2.
function canonicalize (method, host, path, params, date) {
return [
date,
Expand All @@ -72,7 +73,21 @@ function canonicalize (method, host, path, params, date) {
].join('\n')
}

// Canonicalization format is version 5.
function canonicalizeV5 (method, host, path, params, date, body) {
return [
date,
method.toUpperCase(),
host.toLowerCase(),
path,
canonParams(params),
hashString(body),
hashString('') // additional headers not needed at this time
].join('\n')
}

// Return the Authorization header for an HMAC signed request.
// Signature format is version 2.
function sign (ikey, skey, method, host, path, params, date) {
var canon = canonicalize(method, host, path, params, date)
jaherne-duo marked this conversation as resolved.
Show resolved Hide resolved
var sig = crypto.createHmac('sha512', skey)
Expand All @@ -83,8 +98,26 @@ function sign (ikey, skey, method, host, path, params, date) {
return 'Basic ' + auth
}

// Signature format is version 5.
function signV5 (ikey, skey, method, host, path, params, date, body) {
var canon = canonicalizeV5(method, host, path, params, date, body)
var sig = crypto.createHmac('sha512', skey)
.update(canon)
.digest('hex')

var auth = Buffer.from([ikey, sig].join(':')).toString('base64')
return 'Basic ' + auth
}

function hashString (to_hash) {
return crypto.createHash('sha512')
.update(to_hash)
.digest('hex')
}

module.exports = {
'sign': sign,
'signV5': signV5,
'_canonParams': canonParams,
'_canonicalize': canonicalize
}
42 changes: 31 additions & 11 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ var querystring = require('querystring')
var constants = require('./constants')

const _PACKAGE_VERSION = require('../package.json').version
const SIGNATURE_VERSION_2 = 2
const SIGNATURE_VERSION_5 = 5

function Client (ikey, skey, host) {
function Client (ikey, skey, host, sigVersion = SIGNATURE_VERSION_2) {
this.ikey = ikey
this.skey = skey
this.host = host
this.sigVersion = sigVersion
}

Client.prototype.apiCall = function (method, path, params, callback) {
Expand All @@ -18,14 +21,29 @@ Client.prototype.apiCall = function (method, path, params, callback) {
'Host': this.host,
'User-Agent': `duo_api_nodejs/${_PACKAGE_VERSION}`
}
headers['Authorization'] = duo_sig.sign(
this.ikey, this.skey, method, this.host, path, params, date)

var qs = querystring.stringify(params)
var body = ''
if (method === 'POST' || method === 'PUT') {
body = qs
headers['Content-type'] = 'application/x-www-form-urlencoded'
var params_go_in_body = ['POST', 'PUT', 'PATCH'].includes(method)
var qs = querystring.stringify(params)

if (this.sigVersion === SIGNATURE_VERSION_5) {
if (params_go_in_body) {
body = JSON.stringify(params)
params = {}
}
headers['Authorization'] = duo_sig.signV5(
this.ikey, this.skey, method, this.host, path, params, date, body)
} else {
headers['Authorization'] = duo_sig.sign(
this.ikey, this.skey, method, this.host, path, params, date)
}

if (params_go_in_body) {
if (this.sigVersion === SIGNATURE_VERSION_5) {
headers['Content-Type'] = 'application/json'
} else {
headers['Content-Type'] = 'application/x-www-form-urlencoded'
body = qs
}
} else if (qs) {
path += '?' + qs
}
Expand Down Expand Up @@ -68,12 +86,14 @@ function _request_with_backoff (options, body, callback, waitSecs = 1) {
req.end()
}

Client.prototype.jsonApiCall = function (method, path, params, callback) {
Client.prototype.jsonApiCall = function (method, path, params, callback, signature = 2) {
this.apiCall(method, path, params, function (data) {
callback(JSON.parse(data))
})
}, signature)
}

module.exports = {
'Client': Client
'Client': Client,
'SIGNATURE_VERSION_2': SIGNATURE_VERSION_2,
'SIGNATURE_VERSION_5': SIGNATURE_VERSION_5
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
"eslint-plugin-standard": "^3.0.1",
"mocha": "^5.2.0",
"nock": "^9.6.1",
"sinon": "^7.2.7"
"sinon": "^7.2.7",
"chai": "~4.1.2"
},
"optionalDependencies": {
"nopt": ">= 2.1.2"
"nopt": "^2.1.2"
},
"dependencies": {},
"keywords": [
"Duo Security",
"Two-Factor Authentication"
Expand Down
43 changes: 43 additions & 0 deletions tests/duo_sig.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,46 @@ describe('Query Parameter Checks', function () {
done()
})
})

describe('Signature Checks', function () {
it('V2 signature', function (done) {
var ikey = 'test_ikey'
var skey = 'test_skey'
var method = 'POST'
var host = 'test.duosecurity.com'
var path = '/test/v1'
var params = {
'realname': 'First Last',
'username': 'root'
}
var date = 'Tue, 21 Aug 2012 17:29:18 -0000'
var exp_sig = 'Basic dGVzdF9pa2V5OjA0MmEzMGM2ODJiZDgzNmZjNWJhNDY3ODkxYTk1NmVhYTEwMDkxY2EwZjczYTdmYjM5NmFlZmQ1NmEzNTI5ZTU2OTMyZGVhNDI3YTY3MjEzYWY2NjRiYTY4NDE2ODlmZjIzMzZiN2EzNzM3NjZlYTVhYjdhYjlmZTI2MzgyMjZi'

assert.equal(
duo_api.sign(ikey, skey, method, host, path, params, date),
exp_sig
)
done()
})

it('V5 signature', function (done) {
var ikey = 'test_ikey'
var skey = 'test_skey'
var method = 'POST'
var host = 'test.duosecurity.com'
var path = '/test/v1'
var params = {}
var date = 'Tue, 21 Aug 2012 17:29:18 -0000'
var exp_sig = 'Basic dGVzdF9pa2V5OjdkMDI0MDlhMTUyNzY0ODQzY2NjZDgyODRkYTE1M2IzZmI0NDZiYWFkNWY5OTg4ODYzMjVlMjRiYzljZDRhMjQ0ZGU4NWFkNGJmYTdlYTI4NWQ2ODIwOWYxNjA4MzU2NzNkOGI0ZjFlMWIyM2Q5Y2Q1MjFkMjZiZmU3ZjM2NmE0'
var body = JSON.stringify({
'realname': 'First Last',
'username': 'root'
})

assert.equal(
duo_api.signV5(ikey, skey, method, host, path, params, date, body),
exp_sig
)
done()
})
})
Loading
Loading