anvil-connect-nodejs
Version:
Anvil Connect JavaScript client for Node.js
464 lines (331 loc) • 14.8 kB
text/coffeescript
# Test dependencies
fs = require 'fs'
cwd = process.cwd()
path = require 'path'
chai = require 'chai'
sinon = require 'sinon'
sinonChai = require 'sinon-chai'
expect = chai.expect
# Assertions
chai.use sinonChai
chai.should()
# Code under test
IDToken = require path.join cwd, 'lib/IDToken'
IDTokenError = require path.join cwd, 'lib/IDTokenError'
JWT = require 'anvil-connect-jwt'
base64url = require 'base64url'
nowSeconds = require(path.join cwd, 'lib/time-utils').nowSeconds
# 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')
# OpenID Connect Core 1.0
#
# http://openid.net/specs/openid-connect-core-1_0.html#IDToken
#
# 2. ID Token
#
# The primary extension that OpenID Connect makes to OAuth 2.0 to enable
# End-Users to be Authenticated is the ID Token data structure. The ID Token is a
# security token that contains Claims about the Authentication of an End-User by
# an Authorization Server when using a Client, and potentially other requested
# Claims. The ID Token is represented as a JSON Web Token (JWT) [JWT].
#
# The following Claims are used within the ID Token for all OAuth 2.0 flows
# used by OpenID Connect:
#
# iss
# REQUIRED. Issuer Identifier for the Issuer of the response. The iss
# value is a case sensitive URL using the https scheme that contains scheme,
# host, and optionally, port number and path components and no query or
# fragment components.
# sub
# REQUIRED. Subject Identifier. A locally unique and never reassigned
# identifier within the Issuer for the End-User, which is intended to be
# consumed by the Client, e.g., 24400320 or
# AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT exceed 255 ASCII
# characters in length. The sub value is a case sensitive string.
# aud
# REQUIRED. Audience(s) that this ID Token is intended for. It MUST
# contain the OAuth 2.0 client_id of the Relying Party as an audience value.
# It MAY also contain identifiers for other audiences. In the general
# case, the aud value is an array of case sensitive strings. In the
# common special case when there is one audience, the aud value MAY be a
# single case sensitive string.
# exp
# REQUIRED. Expiration time on or after which the ID Token MUST NOT be
# accepted for processing. The processing of this parameter requires that
# the current date/time MUST be before the expiration date/time listed in
# the value. Implementers MAY provide for some small leeway, usually no more
# than a few minutes, to account for clock skew. Its value is a JSON number
# representing the number of seconds from 1970-01-01T0:0:0Z as measured in
# UTC until the date/time. See RFC 3339 [RFC3339] for details regarding
# date/times in general and UTC in particular.
# iat
# REQUIRED. Time at which the JWT was issued. Its value is a JSON number
# representing the number of seconds from 1970-01-01T0:0:0Z as measured in
# UTC until the date/time.
# auth_time
# Time when the End-User authentication occurred. Its value is a JSON
# number representing the number of seconds from 1970-01-01T0:0:0Z as
# measured in UTC until the date/time. When a max_age request is made or
# when auth_time is requested as an Essential Claim, then this Claim is
# REQUIRED; otherwise, its inclusion is OPTIONAL. (The auth_time Claim
# semantically corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] auth_time
# response parameter.)
# nonce
# String value used to associate a Client session with an ID Token, and
# to mitigate replay attacks. The value is passed through unmodified from
# the Authentication Request to the ID Token. If present in the ID Token,
# Clients MUST verify that the nonce Claim Value is equal to the value of
# the nonce parameter sent in the Authentication Request. If present in the
# Authentication Request, Authorization Servers MUST include a nonce Claim
# in the ID Token with the Claim Value being the nonce value sent in the
# Authentication Request. Authorization Servers SHOULD perform no other
# processing on nonce values used. The nonce value is a case sensitive
# string.
# acr
# OPTIONAL. Authentication Context Class Reference. String specifying an
# Authentication Context Class Reference value that identifies the
# Authentication Context Class that the authentication performed satisfied.
# The value "0" indicates the End-User authentication did not meet the
# requirements of ISO/IEC 29115 [ISO29115] level 1. Authentication using a
# long-lived browser cookie, for instance, is one example where the use of
# "level 0" is appropriate. Authentications with level 0 SHOULD NOT be used
# to authorize access to any resource of any monetary value. (This
# corresponds to the OpenID 2.0 PAPE [OpenID.PAPE] nist_auth_level 0.) An
# absolute URI or an RFC 6711 [RFC6711] registered name SHOULD be used as
# the acr value; registered names MUST NOT be used with a different meaning
# than that which is registered. Parties using this claim will need to agree
# upon the meanings of the values used, which may be context-specific. The
# acr value is a case sensitive string.
# amr
# OPTIONAL. Authentication Methods References. JSON array of strings that
# are identifiers for authentication methods used in the authentication. For
# instance, values might indicate that both password and OTP authentication
# methods were used. The definition of particular values to be used in the
# amr Claim is beyond the scope of this specification. Parties using this
# claim will need to agree upon the meanings of the values used, which may be
# context-specific. The amr value is an array of case sensitive strings.
# azp
# OPTIONAL. Authorized party - the party to which the ID Token was
# issued. If present, it MUST contain the OAuth 2.0 Client ID of this party.
# This Claim is only needed when the ID Token has a single audience value
# and that audience is different than the authorized party. It MAY be
# included even when the authorized party is the same as the sole audience.
# The azp value is a case sensitive string containing a StringOrURI value.
#
# ID Tokens MAY contain other Claims. Any Claims used that are not understood
# MUST be ignored. See Sections 3.1.3.6, 3.3.2.11, 5.1, and 7.4 for additional
# Claims defined by this specification.
#
# ID Tokens MUST be signed using JWS [JWS] and optionally both signed and then
# encrypted using JWS [JWS] and JWE [JWE] respectively, thereby providing
# authentication, integrity, non-repudiation, and optionally, confidentiality,
# per Section 16.14. If the ID Token is encrypted, it MUST be signed then
# encrypted, with the result being a Nested JWT, as defined in [JWT]. ID Tokens
# MUST NOT use none as the alg value unless the Response Type used returns no ID
# Token from the Authorization Endpoint (such as when using the Authorization
# Code Flow) and the Client explicitly requested the use of none at Registration
# time.
#
# ID Tokens SHOULD NOT use the JWS or JWE x5u, x5c, jku, or jwk header
# parameter fields. Instead, references to keys used are communicated in advance
# using Discovery and Registration parameters, per Section 10.
#
# The following is a non-normative example of the set of Claims (the JWT Claims
# Set) in an ID Token:
#
# {
# "iss": "https://server.example.com",
# "sub": "24400320",
# "aud": "s6BhdRkqt3",
# "nonce": "n-0S6_WzA2Mj",
# "exp": 1311281970,
# "iat": 1311280970,
# "auth_time": 1311280969,
# "acr": "urn:mace:incommon:iap:silver"
# }
describe 'ID Token', ->
it 'should be a subclass of JWT', ->
IDToken.super.should.equal JWT
describe 'header', ->
it 'must not use "none" as "alg" value', ->
expect(-> new IDToken({}, { 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 =
iss: 'http://anvil.io'
sub: 'uuid'
aud: 'uuid'
exp: Date.now()
iat: Date.now()
token = new IDToken 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 "iss" Issuer Identifier', ->
IDToken.registeredClaims.iss.required.should.be.true
it 'should require "sub" Subject Identifier', ->
IDToken.registeredClaims.sub.required.should.be.true
it 'should require "aud" Audience array or string'
it 'should require "exp" Expiration time', ->
IDToken.registeredClaims.exp.required.should.be.true
it 'should default "exp" to 24 hours', ->
payload =
iss: 'http://anvil.io'
sub: 'uuid'
aud: 'uuid'
token = new IDToken payload
token.payload.exp.should.be.a.number
new Date(token.payload.exp * 1000).getDay().should.not.equal new Date().getDay()
it 'should require "iat" Issued time', ->
IDToken.registeredClaims.iat.required.should.be.true
it 'should default "iat" to now', ->
payload =
iss: 'http://anvil.io'
sub: 'uuid'
aud: 'uuid'
exp: Date.now()
token = new IDToken payload
token.payload.iat.should.be.a.number
it 'should conditionally require "auth_time"'
it 'should include "nonce"', ->
IDToken.registeredClaims.nonce.should.be.defined
it 'should optionally include "acr"', ->
IDToken.registeredClaims.acr.should.be.defined
it 'should optionally include "amr"'
it 'should optionally include "azp"'
it 'should optionally include other claims defined by the specification'
describe 'JWT encoded representation', ->
it 'must be signed using JWS'
describe 'encrypted representation', ->
it 'must be a nested JWT'
it 'must be signed using JWS'
describe 'persistence', ->
describe 'logging', ->
describe 'issuance', ->
describe 'verify', ->
{err,jwt,token} = {}
describe 'with "iss" claim mismatch', ->
before (done) ->
payload =
iss: 'https://WRONG.authorization.server'
sub: 'uuid'
aud: 'https://some.client.app'
jwt = (new IDToken payload).encode(privateKey)
options =
issuer: 'https://your.authorization.server'
key: publicKey
IDToken.verify jwt, options, (error, idtoken) ->
err = error
token = idtoken
done()
it 'should provide an error', ->
expect(err).to.be.instanceof IDTokenError
it 'should not provide a decoded token', ->
expect(token).to.be.undefined
describe 'with "aud" claim mismatch', ->
before (done) ->
payload =
iss: 'https://your.authorization.server'
sub: 'uuid'
aud: 'https://WRONG.client.app'
jwt = (new IDToken payload).encode(privateKey)
options =
iss: payload.iss
aud: 'https://your.client.app'
key: publicKey
IDToken.verify jwt, options, (error, idtoken) ->
err = error
token = idtoken
done()
it 'should provide an error', ->
expect(err).to.be.instanceof IDTokenError
it 'should not provide a decoded token', ->
expect(token).to.be.undefined
describe 'with "azp" claim mismatch', ->
it 'should provide an error'
it 'should not provide a decoded token'
describe 'with "alg" header mismatch', ->
before (done) ->
payload =
iss: 'https://your.authorization.server'
sub: 'uuid'
aud: 'https://your.client.app'
jwt = (new IDToken payload).encode(privateKey)
options =
iss: payload.iss
aud: payload.aud
alg: 'ES256'
key: publicKey
IDToken.verify jwt, options, (error, idtoken) ->
err = error
token = idtoken
done()
it 'should provide an error', ->
expect(err).to.be.instanceof IDTokenError
it 'should not provide a decoded token', ->
expect(token).to.be.undefined
describe 'with expired token', ->
before (done) ->
payload =
iss: 'https://your.authorization.server'
sub: 'uuid'
exp: nowSeconds(-1000)
aud: 'https://your.client.app'
jwt = (new IDToken payload).encode(privateKey)
options =
iss: payload.iss
aud: payload.aud
key: publicKey
IDToken.verify jwt, options, (error, idtoken) ->
err = error
token = idtoken
done()
it 'should provide an error', ->
expect(err).to.be.instanceof IDTokenError
it 'should not provide a decoded token', ->
expect(token).to.be.undefined
describe 'with invalid "iat" claim', ->
it 'should provide an error'
it 'should not provide a decoded token'
describe 'with missing "nonce" claim', ->
it 'should provide an error'
it 'should not provide a decoded token'
describe 'with "nonce" claim mismatch', ->
it 'should provide an error'
it 'should not provide a decoded token'
describe 'with invalid "acr" claim', ->
it 'should provide an error'
it 'should not provide a decoded token'
describe 'with invalid "max_age"', ->
it 'should provide an error'
it 'should not provide a decoded token'
describe 'with valid token', ->
before (done) ->
payload =
iss: 'https://your.authorization.server'
sub: 'uuid'
aud: 'https://your.client.app'
jwt = (new IDToken payload).encode(privateKey)
options =
iss: payload.iss
aud: payload.aud
key: publicKey
IDToken.verify jwt, options, (error, idtoken) ->
err = error
token = idtoken
done()
it 'should not provide an error', ->
expect(err).to.be.null
it 'should not provide a decoded token', ->
expect(token).to.be.instanceof IDToken