anvil-connect-nodejs
Version:
Anvil Connect JavaScript client for Node.js
687 lines (496 loc) • 17.6 kB
text/coffeescript
# Test dependencies
fs = require 'fs'
cwd = process.cwd()
path = require 'path'
chai = require 'chai'
sinon = require 'sinon'
sinonChai = require 'sinon-chai'
nock = require 'nock'
expect = chai.expect
# Assertions
chai.use sinonChai
chai.should()
# Code under test
AccessToken = require path.join cwd, 'lib/AccessToken'
JWT = require 'anvil-connect-jwt'
base64url = require 'base64url'
nowSeconds = require(path.join cwd, 'lib/time-utils').nowSeconds
pem2jwk = require('pem-jwk').pem2jwk
# Keypair
publicFile = path.join cwd, 'test/lib/keys/public.pem'
publicKey = fs.readFileSync(publicFile).toString('ascii')
privateFile = path.join cwd, 'test/lib/keys/private.pem'
privateKey = fs.readFileSync(privateFile).toString('ascii')
jwk = pem2jwk(publicKey)
describe 'Access Token', ->
it 'should be a subclass of JWT', ->
AccessToken.super.should.equal JWT
describe 'header', ->
it 'must not use "none" as "alg" value', ->
expect(-> new AccessToken({}, { alg: 'none' })).to.throw Error
it 'should not use "x5u", "x5c", "jku", or "jwk" header parameter fields', ->
header =
alg: 'RS256'
x5u: 'x5u'
x5c: 'x5c'
jku: 'jku'
jwk: 'jwk'
payload =
jti: 'random'
iss: 'http://anvil.io'
sub: 'uuid'
aud: 'uuid'
exp: Date.now()
iat: Date.now()
scope: 'openid profile'
token = new AccessToken payload, header
expect(token.header.x5u).to.be.undefined
expect(token.header.x5c).to.be.undefined
expect(token.header.jku).to.be.undefined
expect(token.header.jwk).to.be.undefined
describe 'claims', ->
it 'should require "jti" property', ->
AccessToken.registeredClaims.jti.required.should.be.true
it 'should require "iss" Issuer Identifier', ->
AccessToken.registeredClaims.iss.required.should.be.true
it 'should require "iat" Issued time', ->
AccessToken.registeredClaims.iat.required.should.be.true
it 'should require "exp" Expiration time', ->
AccessToken.registeredClaims.exp.required.should.be.true
it 'should require "sub" Subject Identifier', ->
AccessToken.registeredClaims.sub.required.should.be.true
it 'should require "aud" Audience Identifier', ->
AccessToken.registeredClaims.aud.required.should.be.true
it 'should require "scope" property', ->
AccessToken.registeredClaims.scope.required.should.be.true
describe 'refresh', ->
{err,token} = {}
describe 'without refresh_token', ->
it 'should provide an error', ->
AccessToken.refresh null, {client_id:'fakeid', client_secret:'fakesecret'}, (err) ->
err.should.be.instanceof Error
describe 'without client_id', ->
it 'should provide an error', ->
AccessToken.refresh 'faketoken', {client_secret:'fakesecret'}, (err) ->
err.should.be.instanceof Error
describe 'without client_secret', ->
it 'should provide an error', ->
AccessToken.refresh 'faketoken', {client_id:'fakeid'}, (err) ->
err.should.be.instanceof Error
describe 'with all params and response ko', ->
before (done) ->
issuer = 'https://connect.anvil.io'
options =
issuer: issuer
client_id: 'uuid2'
client_secret: 'secret'
headers =
reqHeaders:
'Content-Type': 'application/json'
nock(issuer, headers)
.post('/token', { refresh_token: 'faketoken', grant_type:'refresh_token'})
.basicAuth({
user: options.client_id
pass: options.client_secret
})
.reply(400, {
error: 'invalid_request'
})
AccessToken.refresh 'faketoken', options, (error, accesstoken) ->
err = error
token = accesstoken
done()
after () ->
nock.cleanAll()
it 'should not provide an error', ->
expect(err).to.be.ok
it 'should provide a decoded token', ->
expect(token).not.to.be.ok
describe 'with all params and response ok', ->
tokens =
access_token: 'foo.bar.baz'
refresh_token: 'foo'
before (done) ->
issuer = 'https://connect.anvil.io'
options =
issuer: issuer
client_id: 'uuid2'
client_secret: 'secret'
headers =
reqHeaders:
'Content-Type': 'application/json'
nock(issuer, headers)
.post('/token', { refresh_token: 'faketoken', grant_type:'refresh_token'})
.basicAuth({
user: options.client_id
pass: options.client_secret
})
.reply(200, tokens)
AccessToken.refresh 'faketoken', options, (error, accesstoken) ->
err = error
token = accesstoken
done()
after () ->
nock.cleanAll()
it 'should not provide an error', ->
expect(err).to.not.be.ok
it 'should provide a decoded token', ->
token.should.eql tokens
describe 'verify', ->
{err,token} = {}
describe 'with unverifiable JWT', ->
it 'should provide an error', ->
AccessToken.verify 'mal.formed.jwt', { key: jwk }, (err) ->
err.should.be.instanceof Error
describe 'with verifiable JWT and mismatching issuer', ->
before (done) ->
payload =
jti: 'random'
iss: 'https://WRONG.authorization.server'
sub: 'uuid'
aud: 'https://some.client.app'
exp: Date.now()
iat: Date.now()
scope: 'openid profile'
jwt = (new AccessToken payload).encode(privateKey)
options =
issuer: 'https://your.authorization.server'
key: publicKey
AccessToken.verify jwt, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
it 'should provide an error', ->
expect(err).to.be.instanceof Error
it 'should provide an error description', ->
err.error_description.should.equal 'Mismatching issuer'
it 'should not provide a decoded token', ->
expect(token).to.be.undefined
describe 'with verifiable JWT and mismatching audience', ->
before (done) ->
payload =
jti: 'random'
iss: 'http://your.authorization.server'
sub: 'uuid'
aud: 'https://WRONG.client.app'
exp: Date.now()
iat: Date.now()
scope: 'openid profile'
jwt = (new AccessToken payload).encode(privateKey)
options =
jti: 'random'
issuer: payload.iss
clients: ['https://client.app']
key: jwk
AccessToken.verify jwt, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
it 'should provide an error', ->
expect(err).to.be.instanceof Error
it 'should provide an error description', ->
err.error_description.should.equal 'Mismatching audience'
it 'should not provide a decoded token', ->
expect(token).to.be.undefined
describe 'with verifiable JWT and expired token', ->
before (done) ->
payload =
jti: 'random'
iss: 'https://your.authorization.server'
sub: 'uuid'
exp: nowSeconds(-1000)
iat: Date.now()
aud: 'https://your.client.app'
scope: 'openid profile'
jwt = (new AccessToken payload).encode(privateKey)
options =
issuer: payload.iss
aud: payload.aud
key: publicKey
AccessToken.verify jwt, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
it 'should provide an error', ->
expect(err).to.be.instanceof Error
it 'should provide an error description', ->
err.error_description.should.equal 'Expired access token'
it 'should not provide a decoded token', ->
expect(token).to.be.undefined
describe 'with verifiable JWT and insufficient scope', ->
before (done) ->
payload =
jti: 'random'
iss: 'https://your.authorization.server'
sub: 'uuid'
exp: Date.now()
iat: Date.now()
aud: 'https://your.client.app'
scope: 'openid profile'
jwt = (new AccessToken payload).encode(privateKey)
options =
issuer: payload.iss
aud: payload.aud
key: publicKey
scope: 'realm'
AccessToken.verify jwt, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
it 'should provide an error', ->
expect(err).to.be.instanceof Error
it 'should provide an error description', ->
err.error_description.should.equal 'Insufficient scope'
it 'should not provide a decoded token', ->
expect(token).to.be.undefined
describe 'with verifiable JWT and valid payload', ->
{payload,claims,err} = {}
before (done) ->
payload =
jti: 'random'
iss: 'https://your.authorization.server'
sub: 'uuid'
exp: Date.now()
iat: Date.now()
aud: 'https://your.client.app'
scope: 'openid profile'
jwt = (new AccessToken payload).encode(privateKey)
options =
issuer: payload.iss
aud: payload.aud
key: publicKey
AccessToken.verify jwt, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
it 'should not provide an error', ->
expect(err).not.to.be.ok
it 'should provide a decoded token', ->
token.jti.should.equal payload.jti
token.iss.should.equal payload.iss
token.sub.should.equal payload.sub
token.exp.should.be.a.number
token.iat.should.be.a.number
token.aud.should.equal payload.aud
token.scope.should.equal payload.scope
describe 'with unverifiable random token', ->
before (done) ->
issuer = 'https://connect.anvil.io'
claims =
jti: 'random'
iss: issuer
sub: 'uuid'
exp: Date.now()
iat: Date.now()
aud: 'https://your.client.app'
scope: 'openid profile'
options =
issuer: issuer
client_id: 'uuid2'
client_secret: 'secret'
headers =
reqHeaders:
'Content-Type': 'application/json'
nock(issuer, headers)
.post('/token/verify', { access_token: claims.jti })
.basicAuth({
user: options.client_id
pass: options.client_secret
})
.reply(401, 'Unauthorized')
AccessToken.verify claims.jti, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
after ->
nock.cleanAll()
it 'should provide an error', ->
expect(err).to.be.instanceof Error
it 'should provide an error description', ->
err.message.should.equal 'Unauthorized'
it 'should not provide a decoded token', ->
expect(token).to.not.be.ok
describe 'with verifiable random token and mismatching issuer', ->
before (done) ->
issuer = 'https://connect.anvil.io'
claims =
jti: 'random'
iss: 'https://WRONG.anvil.io'
sub: 'uuid'
exp: Date.now()
iat: Date.now()
aud: 'https://your.client.app'
scope: 'openid profile'
options =
issuer: issuer
client_id: 'uuid2'
client_secret: 'secret'
headers =
reqHeaders:
'Content-Type': 'application/json'
nock(issuer, headers)
.post('/token/verify', { access_token: claims.jti })
.basicAuth({
user: options.client_id
pass: options.client_secret
})
.reply(200, claims)
AccessToken.verify claims.jti, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
after ->
nock.cleanAll()
it 'should provide an error', ->
expect(err).to.be.instanceof Error
it 'should provide an error description', ->
err.error_description.should.equal 'Mismatching issuer'
it 'should not provide a decoded token', ->
expect(token).to.not.be.ok
describe 'with verifiable random token and mismatching audience', ->
before (done) ->
issuer = 'https://connect.anvil.io'
claims =
jti: 'random'
iss: issuer
sub: 'uuid'
exp: Date.now()
iat: Date.now()
aud: 'https://your.client.app'
scope: 'openid profile'
options =
issuer: issuer
client_id: 'uuid2'
client_secret: 'secret'
clients: 'https://not.client.app'
headers =
reqHeaders:
'Content-Type': 'application/json'
nock(issuer, headers)
.post('/token/verify', { access_token: claims.jti })
.basicAuth({
user: options.client_id
pass: options.client_secret
})
.reply(200, claims)
AccessToken.verify claims.jti, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
after ->
nock.cleanAll()
it 'should provide an error', ->
expect(err).to.be.instanceof Error
it 'should provide an error description', ->
err.error_description.should.equal 'Mismatching audience'
it 'should not provide a decoded token', ->
expect(token).to.not.be.ok
describe 'with verifiable random token and expired token', ->
before (done) ->
issuer = 'https://connect.anvil.io'
claims =
jti: 'random'
iss: issuer
sub: 'uuid'
exp: nowSeconds(-1000)
iat: Date.now()
aud: 'https://your.client.app'
scope: 'openid profile'
options =
issuer: issuer
client_id: 'uuid2'
client_secret: 'secret'
headers =
reqHeaders:
'Content-Type': 'application/json'
nock(issuer, headers)
.post('/token/verify', { access_token: claims.jti })
.basicAuth({
user: options.client_id
pass: options.client_secret
})
.reply(200, claims)
AccessToken.verify claims.jti, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
after ->
nock.cleanAll()
it 'should provide an error', ->
expect(err).to.be.instanceof Error
it 'should provide an error description', ->
err.error_description.should.equal 'Expired access token'
it 'should not provide a decoded token', ->
expect(token).to.not.be.ok
describe 'with verifiable random token and insufficient scope', ->
before (done) ->
issuer = 'https://connect.anvil.io'
claims =
jti: 'random'
iss: issuer
sub: 'uuid'
exp: Date.now()
iat: Date.now()
aud: 'https://your.client.app'
scope: 'openid profile'
options =
issuer: issuer
client_id: 'uuid2'
client_secret: 'secret'
scope: 'realm'
headers =
reqHeaders:
'Content-Type': 'application/json'
nock(issuer, headers)
.post('/token/verify', { access_token: claims.jti })
.basicAuth({
user: options.client_id
pass: options.client_secret
})
.reply(200, claims)
AccessToken.verify claims.jti, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
after ->
nock.cleanAll()
it 'should provide an error', ->
expect(err).to.be.instanceof Error
it 'should provide an error description', ->
err.error_description.should.equal 'Insufficient scope'
it 'should not provide a decoded token', ->
expect(token).to.not.be.ok
describe 'with verifiable random token and valid payload', ->
{claims} = {}
before (done) ->
issuer = 'https://connect.anvil.io'
claims =
jti: 'random'
iss: issuer
sub: 'uuid'
exp: Date.now()
iat: Date.now()
aud: 'https://your.client.app'
scope: 'openid profile'
options =
issuer: issuer
client_id: 'uuid2'
client_secret: 'secret'
headers =
reqHeaders:
'Content-Type': 'application/json'
nock(issuer, headers)
.post('/token/verify', { access_token: claims.jti })
.basicAuth({
user: options.client_id
pass: options.client_secret
})
.reply(200, claims)
AccessToken.verify claims.jti, options, (error, accesstoken) ->
err = error
token = accesstoken
done()
it 'should not provide an error', ->
expect(err).to.not.be.ok
it 'should provide a decoded token', ->
token.should.eql claims