├── .nvmrc ├── .eslintignore ├── spec ├── .eslintrc.js └── index.spec.js ├── CHANGELOG.md ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── src ├── buffer-reader.js ├── buffer-writer.js └── index.js ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.13 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | tasks 5 | lib 6 | -------------------------------------------------------------------------------- /spec/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "leankit/test" 3 | }; 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.x 2 | 3 | ### 3.0.0 4 | 5 | * Added extra signature of unencrpyted data required in .NET validation 6 | 7 | ## 2.x 8 | 9 | ### 2.0.0 10 | 11 | * Node 12 support and bignum -> BigInt 12 | 13 | ## 1.x 14 | 15 | ### 1.1.2 16 | 17 | * Remove moar es6 and add use strict 18 | 19 | ### 1.1.1 20 | 21 | * Removed babel and build step 22 | 23 | ### 1.1.0 24 | 25 | * Added encrypt method 26 | * Adding missing mocha dependency 27 | * Fix for linting and test compilation 28 | 29 | ### 1.0.1 30 | 31 | * Added test, lint, format, and build -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ "leankit", "leankit/es6" ], 3 | 4 | rules: { 5 | "no-var": 0, 6 | "vars-on-top": 0, 7 | "init-declarations": 0, 8 | "prefer-const": 0, 9 | "prefer-arrow-callback": 0, 10 | "global-require": 0, 11 | "consistent-return": 0, 12 | "prefer-template": 0, 13 | "no-magic-numbers": 0, 14 | "max-lines": 0, 15 | "max-statements": 0, 16 | "no-invalid-this": 0, 17 | "no-mixed-operators": 0, 18 | strict: 0 19 | }, 20 | env: { 21 | node: true, 22 | browser: false, 23 | commonjs: false 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | lib 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 LeanKit Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/buffer-reader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require( "assert" ); 4 | 5 | const BYTES_PER_CHAR = 2; 6 | const TICKS_IN_MILLISECOND = 10000; 7 | const MILLISECONDS_EPOCH_OFFSET = 62135596800000; 8 | 9 | function BufferReader( buffer ) { 10 | this.buffer = buffer; 11 | this.offset = 0; 12 | } 13 | 14 | BufferReader.prototype = { 15 | skip( bytes ) { 16 | this.offset += bytes; 17 | return this; 18 | }, 19 | 20 | assertByte( expected, description ) { 21 | return assert( this.buffer[ this.offset++ ] === expected, `Invalid ${ description }.` ); 22 | }, 23 | 24 | readByte() { 25 | return this.buffer[ this.offset++ ]; 26 | }, 27 | 28 | readBool() { 29 | return !!this.buffer[ this.offset++ ]; 30 | }, 31 | 32 | readInt64() { 33 | let val = this.buffer.slice( this.offset, this.offset + 8 ).readBigInt64LE(); 34 | this.offset += 8; 35 | return Number( val ); 36 | }, 37 | 38 | readDate() { 39 | return new Date( ( this.readInt64() / TICKS_IN_MILLISECOND ) - MILLISECONDS_EPOCH_OFFSET ); 40 | }, 41 | 42 | readString() { 43 | const length = this.readByte() * BYTES_PER_CHAR; 44 | const val = length ? this.buffer.slice( this.offset, this.offset + length ).toString( "ucs2" ) : null; 45 | this.offset += length; 46 | return val; 47 | } 48 | }; 49 | 50 | module.exports = BufferReader; 51 | -------------------------------------------------------------------------------- /src/buffer-writer.js: -------------------------------------------------------------------------------- 1 | /* global BigInt */ 2 | "use strict"; 3 | 4 | const BYTES_PER_CHAR = 2; 5 | const TICKS_IN_MILLISECOND = 10000; 6 | const MILLISECONDS_EPOCH_OFFSET = 62135596800000; 7 | 8 | function BufferWriter( size ) { 9 | this.buffer = Buffer.alloc( size ); 10 | this.offset = 0; 11 | } 12 | 13 | BufferWriter.prototype = { 14 | writeBuffer( val ) { 15 | val.copy( this.buffer, this.offset ); 16 | this.offset += val.length; 17 | return this; 18 | }, 19 | 20 | writeByte( val ) { 21 | this.buffer[ this.offset++ ] = val; 22 | return this; 23 | }, 24 | 25 | writeBool( val ) { 26 | this.buffer[ this.offset++ ] = val ? 0x01 : 0x00; 27 | return this; 28 | }, 29 | 30 | writeInt64( val ) { 31 | let buf = Buffer.alloc( 8 ); 32 | buf.writeBigInt64LE( BigInt( val ) ); 33 | this.writeBuffer( buf ); 34 | return this; 35 | }, 36 | 37 | writeDate( val ) { 38 | this.writeInt64( BigInt( ( val.getTime() + MILLISECONDS_EPOCH_OFFSET ) * TICKS_IN_MILLISECOND ) ); 39 | return this; 40 | }, 41 | 42 | writeString( val ) { 43 | if ( val && val.length ) { 44 | const length = val.length * BYTES_PER_CHAR; 45 | this.writeByte( val.length ); 46 | this.buffer.write( val, this.offset, "ucs2" ); 47 | this.offset += length; 48 | } else { 49 | this.writeByte( 0 ); 50 | } 51 | return this; 52 | } 53 | }; 54 | 55 | BufferWriter.stringSize = function( val ) { 56 | return ( val && val.length || 0 ) * BYTES_PER_CHAR + 1; 57 | }; 58 | 59 | module.exports = BufferWriter; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aspxauth", 3 | "version": "3.0.0", 4 | "description": "Verify and decrypt .NET's .ASPXAUTH cookie from node", 5 | "main": "src/index.js", 6 | "files": [ 7 | "src" 8 | ], 9 | "scripts": { 10 | "lint": "eslint --fix ./", 11 | "pretest": "npm run lint", 12 | "test": "npm run cover", 13 | "test:only": "mocha spec/*.spec.js", 14 | "test:watch": "npm run test:only -- -w", 15 | "cover": "nyc -r text-summary -r html -- npm run test:only", 16 | "cover:show": "open \"file://$PWD/coverage/index.html\"" 17 | }, 18 | "author": "LeanKit", 19 | "contributors": [ 20 | { 21 | "name": "Matt Dunlap", 22 | "email": "matt.dunlap@leankit.com", 23 | "url": "https://github.com/prestaul" 24 | }, 25 | { 26 | "name": "Josh Bush", 27 | "email": "josh.bush@leankit.com" 28 | } 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/LeanKit-Labs/aspxauth.git" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/LeanKit-Labs/aspxauth/issues" 36 | }, 37 | "homepage": "https://github.com/LeanKit-Labs/aspxauth#readme", 38 | "keywords": [ 39 | ".NET", 40 | "authentication", 41 | "auth", 42 | ".ASPXAUTH", 43 | "FormsAuthentication" 44 | ], 45 | "license": "MIT", 46 | "devDependencies": { 47 | "chai": "^4.2.0", 48 | "eslint": "^6.8.0", 49 | "eslint-config-leankit": "^5.1.0", 50 | "mocha": "^6.2.2", 51 | "nyc": "^15.0.0", 52 | "sinon": "^8.0.1" 53 | }, 54 | "nyc": { 55 | "include": "src" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aspxauth 2 | 3 | **Note:** There are many variables, flags, and version-specific considerations for how .NET generates the `.aspxauth` cookie. This library works for our needs using older versions of the .NET framework. Your milage may vary. 4 | 5 | Provides utilities to assist in generating, validating and decrypting .NET authorization tickets (usually set in the .ASPXAUTH cookie) for interoperation with .NET authentication. 6 | 7 | ## Setup 8 | 9 | The module must be initialized with configuration that corresponds to your .NET configuration and the machine key used to generate the auth ticket. 10 | 11 | - `validationMethod` (string): (default "sha1") 12 | - `validationKey` (string): hex encoded key to use for signature validation 13 | - `decryptionMethod` (string): (default "aes") 14 | - `decryptionIV` (string): hex encoded initialization vector (defaults to a vector of zeros) 15 | - `decryptionKey` (string): hex encoded key to use for decryption 16 | - `ticketVersion` (integer): if specified then will be used to validate the ticket version 17 | - `validateExpiration` (bool): (default `true`) if false then decrypted tickets will be returned even if past their expiration 18 | - `encryptAsBuffer` (bool): (default `false`) if true, encrypt will return a buffer rather than a hex encoded string 19 | - `defaultTTL` (integer): (default 24hrs) if provided is used as milliseconds from `issueDate` to expire generated tickets 20 | - `defaultPersistent` (bool): (default `false`) if provided is used as default `isPersistent` value for generated tickets 21 | - `defaultCookiePath` (string): (default "/") if provided is used as default `cookiePath` for generated tickets 22 | 23 | 24 | ```js 25 | // Configure 26 | var aspxauth = require( "aspxauth" )( { 27 | validationMethod: "sha1", 28 | validationKey: process.env.DOTNET_VALIDATION_KEY, 29 | decryptionMethod: "aes", 30 | decryptionIV: process.env.DOTNET_DECRYPTION_IV, 31 | decryptionKey: process.env.DOTNET_DECRYPTION_KEY 32 | } ); 33 | 34 | // Generate encrypted cookie 35 | var encryptedCookieValue = aspxauth.encrypt( { 36 | name: "some.username@place.com", 37 | customData: "other data" 38 | } ); 39 | 40 | // Decrypt an existing cookie 41 | var authTicket = aspxauth.decrypt( req.cookies[ ".ASPXAUTH" ] ); 42 | ``` 43 | 44 | ### Supported validation methods 45 | 46 | - sha1 47 | 48 | ### Supported decryption methods 49 | 50 | - aes 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require( "assert" ); 4 | const { createCipheriv, createDecipheriv, createHmac, randomBytes } = require( "crypto" ); 5 | const BufferReader = require( "./buffer-reader" ); 6 | const BufferWriter = require( "./buffer-writer" ); 7 | 8 | const VALIDATION_METHODS = { 9 | sha1: { 10 | algorithm: "sha1", 11 | signatureSize: 20 12 | } 13 | }; 14 | 15 | const DECRYPTION_METHODS = { 16 | aes: { 17 | cipher: "aes-256-cbc", 18 | ivSize: 16, 19 | headerSize: 32 20 | } 21 | }; 22 | 23 | const FORMAT_VERSION = 1; 24 | const SPACER = 0xfe; 25 | const FOOTER = 0xff; 26 | 27 | /** 28 | validationMethod (string): (default "sha1") 29 | validationKey (string): hex encoded key to use for signature validation 30 | 31 | decryptionMethod (string): (default "aes") 32 | decryptionIV (string): hex encoded initialization vector (defaults to zeros) 33 | decryptionKey (string): hex encoded key to use for decryption 34 | 35 | ticketVersion (integer): if specified then will be used to validate the ticket version 36 | validateExpiration (bool): (default true) if false then decrypted tickets will be returned even if past their expiration 37 | 38 | encryptAsBuffer (bool): (default false) if true, generate will return a buffer rather than a hex encoded string 39 | defaultTTL (integer): (default 24hrs) if provided is used as milliseconds from issueDate to expire generated tickets 40 | defaultPersistent (bool): (default false) if provided is used as default isPersistent value for generated tickets 41 | defaultCookiePath (string): (default "/") if provided is used as default cookie path for generated tickets 42 | */ 43 | 44 | module.exports = config => { 45 | const VALIDATION_METHOD = VALIDATION_METHODS[ config.validationMethod || "sha1" ]; 46 | const DECRYPTION_METHOD = DECRYPTION_METHODS[ config.decryptionMethod || "aes" ]; 47 | 48 | assert( VALIDATION_METHOD, "Invalid validation method" ); 49 | assert( DECRYPTION_METHOD, "Invalid decryption method" ); 50 | assert( config.validationKey, "'validationKey' is required" ); 51 | assert( config.decryptionKey, "'decryptionKey' is required" ); 52 | 53 | const VALIDATION_KEY = Buffer.from( config.validationKey, "hex" ); 54 | const DECRYPTION_KEY = Buffer.from( config.decryptionKey, "hex" ); 55 | const DECRYPTION_IV = config.decryptionIV ? Buffer.from( config.decryptionIV, "hex" ) : Buffer.alloc( DECRYPTION_METHOD.ivSize ); 56 | 57 | const REQUIRED_VERSION = config.ticketVersion || false; 58 | const VALIDATE_EXPIRATION = config.validateExpiration !== false; 59 | 60 | const AS_BUFFER = !!config.encryptAsBuffer; 61 | const DEFAULT_TTL = config.defaultTTL || 86400000; 62 | const DEFAULT_IS_PERSISTENT = !!config.defaultPersistent; 63 | const DEFAULT_COOKIE_PATH = config.defaultCookiePath || "/"; 64 | 65 | const BASE_PAYLOAD_SIZE = 21; 66 | 67 | function validate( bytes ) { 68 | const signature = bytes.slice( -VALIDATION_METHOD.signatureSize ); 69 | const payload = bytes.slice( 0, -VALIDATION_METHOD.signatureSize ); 70 | 71 | const hash = createHmac( VALIDATION_METHOD.algorithm, VALIDATION_KEY ); 72 | hash.update( payload ); 73 | 74 | return hash.digest().equals( signature ); 75 | } 76 | 77 | function decrypt( cookie ) { 78 | try { 79 | const bytes = cookie instanceof Buffer ? cookie : Buffer.from( cookie, "hex" ); 80 | 81 | if ( !validate( bytes ) ) { 82 | return null; 83 | } 84 | 85 | const decryptor = createDecipheriv( DECRYPTION_METHOD.cipher, DECRYPTION_KEY, DECRYPTION_IV ); 86 | const payload = bytes.slice( 0, -VALIDATION_METHOD.signatureSize ); 87 | const decryptedBytes = Buffer.concat( [ decryptor.update( payload ), decryptor.final() ] ); 88 | 89 | if ( !validate( decryptedBytes.slice( DECRYPTION_METHOD.headerSize ) ) ) { 90 | return null; 91 | } 92 | 93 | const reader = new BufferReader( decryptedBytes ); 94 | const ticket = {}; 95 | 96 | reader.skip( DECRYPTION_METHOD.headerSize ); 97 | reader.assertByte( FORMAT_VERSION, "format version" ); 98 | 99 | if ( REQUIRED_VERSION ) { 100 | reader.assertByte( REQUIRED_VERSION, "ticket version" ); 101 | ticket.ticketVersion = REQUIRED_VERSION; 102 | } else { 103 | ticket.ticketVersion = reader.readByte(); 104 | } 105 | 106 | ticket.issueDate = reader.readDate(); 107 | reader.assertByte( SPACER, "spacer" ); 108 | ticket.expirationDate = reader.readDate(); 109 | 110 | if ( VALIDATE_EXPIRATION && ticket.expirationDate < Date.now() ) { 111 | return null; 112 | } 113 | 114 | ticket.isPersistent = reader.readBool(); 115 | ticket.name = reader.readString(); 116 | ticket.customData = reader.readString(); 117 | ticket.cookiePath = reader.readString(); 118 | reader.assertByte( FOOTER, "footer" ); 119 | 120 | return ticket; 121 | } catch ( e ) { 122 | return null; 123 | } 124 | } 125 | 126 | function encrypt( ticket ) { 127 | const stringsSize = BufferWriter.stringSize( ticket.name ) + BufferWriter.stringSize( ticket.customData ) + BufferWriter.stringSize( ticket.cookiePath || DEFAULT_COOKIE_PATH ); 128 | const writer = new BufferWriter( BASE_PAYLOAD_SIZE + stringsSize ); 129 | 130 | writer.writeByte( FORMAT_VERSION ); 131 | 132 | if ( REQUIRED_VERSION ) { 133 | if ( ticket.ticketVersion ) { 134 | assert( REQUIRED_VERSION === ticket.ticketVersion, `Invalid ticket version ${ ticket.ticketVersion }, expected ${ REQUIRED_VERSION }` ); 135 | } 136 | writer.writeByte( REQUIRED_VERSION ); 137 | } else { 138 | writer.writeByte( ticket.ticketVersion || 0x01 ); 139 | } 140 | 141 | const issueDate = ticket.issueDate || new Date(); 142 | const expirationDate = ticket.expirationDate || new Date( issueDate.getTime() + DEFAULT_TTL ); 143 | writer.writeDate( issueDate ); 144 | writer.writeByte( SPACER ); 145 | writer.writeDate( expirationDate ); 146 | writer.writeBool( "isPersistent" in ticket ? !!ticket.isPersistent : DEFAULT_IS_PERSISTENT ); 147 | writer.writeString( ticket.name ); 148 | writer.writeString( ticket.customData ); 149 | writer.writeString( ticket.cookiePath || DEFAULT_COOKIE_PATH ); 150 | writer.writeByte( FOOTER ); 151 | 152 | // add a hash of the preencrypted bytes 153 | const preEncryptedHash = createHmac( "sha1", VALIDATION_KEY ); 154 | preEncryptedHash.update( writer.buffer ); 155 | const preEncryptedBytes = Buffer.concat( [ randomBytes( DECRYPTION_METHOD.headerSize ), writer.buffer, preEncryptedHash.digest() ] ); 156 | 157 | const encryptor = createCipheriv( DECRYPTION_METHOD.cipher, DECRYPTION_KEY, DECRYPTION_IV ); 158 | const encryptedBytes = Buffer.concat( [ encryptor.update( preEncryptedBytes ), encryptor.final() ] ); 159 | 160 | // add a hash of the encrypted bytes 161 | const hash = createHmac( "sha1", VALIDATION_KEY ); 162 | hash.update( encryptedBytes ); 163 | 164 | const final = Buffer.concat( [ encryptedBytes, hash.digest() ] ); 165 | 166 | return AS_BUFFER ? final : final.toString( "hex" ); 167 | } 168 | 169 | return { decrypt, encrypt }; 170 | }; 171 | -------------------------------------------------------------------------------- /spec/index.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const factory = require( "../src/index" ); 4 | const useFakeTimers = require( "sinon" ).useFakeTimers; 5 | const chai = require( "chai" ); 6 | chai.should(); 7 | 8 | describe( "aspxauth#decrypt", () => { 9 | let config, token, result, clock, currentTime; 10 | 11 | const version2Token = "874B814416D1064EFF67A64D1200525471A0C74ADA3B450F05CBBCC7B2BEC6C9E5D20F1AD4E83DB3C72A965096C9680D4F667DAC7AFDAE168C7EE9D5CF77EBAFBB439CE461500A3E9547883E87B4BA5FEC14F9EECD1AECC594ECA46C2A8FA2652195A6562465E6811E36E2B119EE1A927D11E84E89D3AF6870F00A9C72E65F26A5E8EA7EBDADE8C4CD15BFF433FAAF269F17C4EC85C49069981227416EDD2086FA4D453D25A865580990608F142D167CBC671717C0C22E8E6FC28CA3F1C385462E40B6505AE67EEEFA90EC6BB2E6E7D04B0F6CB457223EAFE6BA0C32D7F867C764D33ABA914ABDACA6FBCF8DB7C5628A72606E3F113794BB071A2F432C9422017ECFEEBFC28C1F1A7AEE12AD3AFA9E7916FF52B041EA6BF44FAD4DBBFD1A6A8B4A4EBF37"; 12 | 13 | const version2TokenContents = { 14 | ticketVersion: 2, 15 | issueDate: new Date( "2016-09-23T15:38:02.250Z" ), 16 | expirationDate: new Date( "2016-10-23T15:38:02.250Z" ), 17 | isPersistent: true, 18 | name: "calvin.bottoms@leankit.com", 19 | customData: "banditsoftware.localkanban.com:59ddc62b-1b40-4fe3-afe3-e1bb11bb8170", 20 | cookiePath: "/" 21 | }; 22 | 23 | function configAndDecrypt() { 24 | clock = useFakeTimers( currentTime.getTime() ); 25 | result = factory( config ).decrypt( token ); 26 | } 27 | 28 | beforeEach( () => { 29 | config = { 30 | validationMethod: "sha1", 31 | validationKey: "709FC62CDB7CC79821DEBB2062FDED6795AD8CB37341B55B3763923BEEF662865AF7EC613F9A76171CA3C336ED119D1C103555D87D092BAD4A63F807592B0520", 32 | decryptionMethod: "aes", 33 | decryptionIV: "00000000000000000000000000000000", 34 | decryptionKey: "9DA83917EE2DE9008FCB45986195A9BC11EF9496D67042C76B4052CEFA22EF45", 35 | ticketVersion: 2 36 | }; 37 | token = version2Token; 38 | currentTime = new Date( 2016, 9, 1 ); 39 | } ); 40 | 41 | afterEach( () => { 42 | clock.restore(); 43 | } ); 44 | 45 | describe( "when configuration and token are valid", () => { 46 | it( "should return ticket object", function() { 47 | configAndDecrypt(); 48 | result.should.eql( version2TokenContents ); 49 | } ); 50 | } ); 51 | 52 | describe( "when token is not valid", () => { 53 | it( "should return null", function() { 54 | token = "87"; 55 | configAndDecrypt(); 56 | ( result === null ).should.be.true; // eslint-disable-line no-unused-expressions 57 | } ); 58 | } ); 59 | 60 | describe( "when signature on unencrypted bytes does not match hash", () => { 61 | it( "should return null", function() { 62 | token = "d522006c09b3cdc7cc77e5fce18e4049a4a0532c60111c35abf1c431ed36deebbc0eaf50f2c085e504f59e41c4f17952c1fbdf3afccc530da188b2577ff0aa65dea41e2cce59b6b657c3f5f869c52a75f0aa0a22f700656b5b7ba01b3f39a64f8b36742ad82fc53073556e8b87eb12ccb04ed027f4cd07aa726519e54a7112c324306c41b1a9df8f06b04aebf07e0a881b02a5981555040bc63fe3410210f0bbfd26ada59f5cc73e5dfe7bc52faef2be82f5699891ace2ac8f84104b67f9a6e927a12791be72479e2a63732c457479228c3c8be34fdc880e44d0b6e46b909c1f8df7921fb320b166f4a54bfc276325afe4504ea9e4b270050676447dd6d4f6d7f96ce61562e166ceb56c1d0d7df5e269775ed4f553cf7726b75426d6164f0cfff973225c"; 63 | configAndDecrypt(); 64 | ( result === null ).should.be.true; // eslint-disable-line no-unused-expressions 65 | } ); 66 | } ); 67 | 68 | describe( "when token is a buffer", () => { 69 | it( "should return token object", function() { 70 | token = Buffer.from( token, "hex" ); 71 | configAndDecrypt(); 72 | result.should.eql( version2TokenContents ); 73 | } ); 74 | } ); 75 | 76 | describe( "when ticket version is not provided", () => { 77 | it( "should not check ticket version", () => { 78 | delete config.ticketVersion; 79 | configAndDecrypt(); 80 | result.should.eql( version2TokenContents ); 81 | } ); 82 | } ); 83 | 84 | describe( "when specified ticket version does not match", () => { 85 | it( "should return null", () => { 86 | config.ticketVersion = 3; 87 | configAndDecrypt(); 88 | ( result === null ).should.be.true; // eslint-disable-line no-unused-expressions 89 | } ); 90 | } ); 91 | 92 | describe( "when validation method is not specified", () => { 93 | it( "should use default of sha1", () => { 94 | delete config.validationMethod; 95 | configAndDecrypt(); 96 | result.should.eql( version2TokenContents ); 97 | } ); 98 | } ); 99 | 100 | describe( "when specified validation method is not supported", () => { 101 | it( "should return null", () => { 102 | config.validationMethod = "no-good"; 103 | configAndDecrypt.should.throw( Error ); 104 | } ); 105 | } ); 106 | 107 | describe( "when decryption method is not specified", () => { 108 | it( "should use default of aes", () => { 109 | delete config.decryptionMethod; 110 | configAndDecrypt(); 111 | result.should.eql( version2TokenContents ); 112 | } ); 113 | } ); 114 | 115 | describe( "when specified decryption method is not supported", () => { 116 | it( "should return null", () => { 117 | config.decryptionMethod = "no-good"; 118 | configAndDecrypt.should.throw( Error ); 119 | } ); 120 | } ); 121 | 122 | describe( "when initialization vector is not specified", () => { 123 | it( "should use default", () => { 124 | delete config.decryptionIV; 125 | configAndDecrypt(); 126 | result.should.eql( version2TokenContents ); 127 | } ); 128 | } ); 129 | 130 | describe( "when token is expired", () => { 131 | it( "should return null", () => { 132 | currentTime.setFullYear( currentTime.getFullYear() + 1 ); 133 | configAndDecrypt(); 134 | ( result === null ).should.be.true; // eslint-disable-line no-unused-expressions 135 | } ); 136 | } ); 137 | 138 | describe( "when token is expired but validateExpiration is false", () => { 139 | it( "should return ticket object", () => { 140 | currentTime.setFullYear( currentTime.getFullYear() + 1 ); 141 | config.validateExpiration = false; 142 | configAndDecrypt(); 143 | result.should.eql( version2TokenContents ); 144 | } ); 145 | } ); 146 | } ); 147 | 148 | describe( "aspxauth#encrypt", () => { 149 | let aspxauth; 150 | 151 | function configure( opts ) { 152 | aspxauth = factory( Object.assign( { 153 | validationKey: "709FC62CDB7CC79821DEBB2062FDED6795AD8CB37341B55B3763923BEEF662865AF7EC613F9A76171CA3C336ED119D1C103555D87D092BAD4A63F807592B0520", 154 | decryptionKey: "9DA83917EE2DE9008FCB45986195A9BC11EF9496D67042C76B4052CEFA22EF45", 155 | validateExpiration: false 156 | }, opts ) ); 157 | } 158 | 159 | function encrypt( ticket ) { 160 | return aspxauth.encrypt( Object.assign( { 161 | name: "fake name" 162 | }, ticket ) ); 163 | } 164 | 165 | function test( incomingTicket, expectedTicket ) { 166 | const cookie = encrypt( incomingTicket ); 167 | cookie.should.be.a( "string" ).and.match( /^[a-f0-9]+$/ ); 168 | 169 | const actualTicket = aspxauth.decrypt( cookie ); 170 | actualTicket.should.be.an( "object" ); 171 | 172 | Object.keys( expectedTicket ).forEach( key => { 173 | if ( expectedTicket[ key ] instanceof Date ) { 174 | actualTicket[ key ].should.be.a( "date", `Expected ticket to have property ${ key } of type Date` ); 175 | actualTicket[ key ].getTime().should.equal( expectedTicket[ key ].getTime(), `Expected ticket to have property ${ key } of ${ expectedTicket[ key ] }, but got ${ actualTicket[ key ] }` ); 176 | } else { 177 | actualTicket.should.have.property( key, expectedTicket[ key ] ); 178 | } 179 | } ); 180 | } 181 | 182 | describe( "with no optional configuration", () => { 183 | before( () => { 184 | configure( {} ); 185 | } ); 186 | 187 | describe( "when issueDate is set", () => { 188 | it( "should use the set date", () => { 189 | const ticket = { issueDate: new Date( 2016, 0, 1, 0, 0, 0 ) }; 190 | test( ticket, ticket ); 191 | } ); 192 | 193 | describe( "and expirationDate is missing", () => { 194 | it( "should use a 24 hour ttl", () => { 195 | const ticket = { issueDate: new Date( 2016, 0, 1, 0, 0, 0 ) }; 196 | test( ticket, { expirationDate: new Date( 2016, 0, 2, 0, 0, 0 ) } ); 197 | } ); 198 | } ); 199 | 200 | describe( "and expirationDate is set", () => { 201 | it( "should use the set expiration", () => { 202 | const ticket = { issueDate: new Date( 2016, 0, 1, 0, 0, 0 ), expirationDate: new Date( 2017, 0, 1, 0, 0, 0 ) }; 203 | test( ticket, { expirationDate: new Date( 2017, 0, 1, 0, 0, 0 ) } ); 204 | } ); 205 | } ); 206 | } ); 207 | 208 | describe( "with no issueDate", () => { 209 | let clocks; 210 | 211 | before( () => { 212 | clocks = useFakeTimers(); 213 | } ); 214 | 215 | after( () => { 216 | clocks.restore(); 217 | } ); 218 | 219 | it( "should use the current date/time", () => { 220 | test( {}, { issueDate: new Date() } ); 221 | } ); 222 | 223 | describe( "and expirationDate is missing", () => { 224 | it( "should use a 24 hour ttl", () => { 225 | const tomorrow = new Date(); 226 | tomorrow.setDate( tomorrow.getDate() + 1 ); 227 | test( {}, { expirationDate: tomorrow } ); 228 | } ); 229 | } ); 230 | 231 | describe( "and expirationDate is set", () => { 232 | it( "should use the set expiration", () => { 233 | const ticket = { expirationDate: new Date( 2017, 0, 1, 0, 0, 0 ) }; 234 | test( ticket, { expirationDate: new Date( 2017, 0, 1, 0, 0, 0 ) } ); 235 | } ); 236 | } ); 237 | } ); 238 | 239 | describe( "when isPersistent is missing", () => { 240 | it( "should use the default", () => { 241 | test( {}, { isPersistent: false } ); 242 | } ); 243 | } ); 244 | 245 | describe( "when isPersistent is set", () => { 246 | it( "should use the set value", () => { 247 | const ticket = { isPersistent: true }; 248 | test( ticket, ticket ); 249 | } ); 250 | } ); 251 | 252 | describe( "when cookiePath is missing", () => { 253 | it( "should use \"/\"", () => { 254 | test( {}, { cookiePath: "/" } ); 255 | } ); 256 | } ); 257 | 258 | describe( "when cookiePath is set", () => { 259 | it( "should use the set cookiePath", () => { 260 | const ticket = { cookiePath: "/to/grandmothers/house" }; 261 | test( ticket, ticket ); 262 | } ); 263 | } ); 264 | 265 | describe( "when ticketVersion is missing", () => { 266 | it( "should use version one", () => { 267 | test( {}, { ticketVersion: 1 } ); 268 | } ); 269 | } ); 270 | 271 | describe( "when ticketVersion is set", () => { 272 | it( "should add the set version", () => { 273 | const ticket = { ticketVersion: 7 }; 274 | test( ticket, ticket ); 275 | } ); 276 | } ); 277 | } ); 278 | 279 | describe( "with defaultTTL", () => { 280 | before( () => { 281 | configure( { defaultTTL: 60000 } ); 282 | } ); 283 | 284 | describe( "when expirationDate is missing", () => { 285 | it( "should use the new default TTL", () => { 286 | const ticket = { issueDate: new Date( 2016, 0, 1, 0, 0, 0 ) }; 287 | test( ticket, { expirationDate: new Date( 2016, 0, 1, 0, 1, 0 ) } ); 288 | } ); 289 | } ); 290 | 291 | describe( "when expirationDate is set", () => { 292 | it( "should use the set expiration", () => { 293 | const ticket = { expirationDate: new Date( 2017, 0, 1 ) }; 294 | test( ticket, ticket ); 295 | } ); 296 | } ); 297 | } ); 298 | 299 | describe( "with defaultPersistent", () => { 300 | before( () => { 301 | configure( { defaultPersistent: true } ); 302 | } ); 303 | 304 | describe( "when isPersistent is missing", () => { 305 | it( "should use the new default", () => { 306 | test( {}, { isPersistent: true } ); 307 | } ); 308 | } ); 309 | 310 | describe( "when isPersistent is set", () => { 311 | it( "should use the set value", () => { 312 | const ticket = { isPersistent: false }; 313 | test( ticket, ticket ); 314 | } ); 315 | } ); 316 | } ); 317 | 318 | describe( "with defaultCookiePath", () => { 319 | before( () => { 320 | configure( { defaultCookiePath: "/to/grandmothers/house" } ); 321 | } ); 322 | 323 | describe( "when cookiePath is missing", () => { 324 | it( "should use the default cookiePath", () => { 325 | test( {}, { cookiePath: "/to/grandmothers/house" } ); 326 | } ); 327 | } ); 328 | 329 | describe( "when cookiePath is set", () => { 330 | it( "should use the set cookiePath", () => { 331 | const ticket = { cookiePath: "/home/again" }; 332 | test( ticket, ticket ); 333 | } ); 334 | } ); 335 | } ); 336 | 337 | describe( "with required version", () => { 338 | before( () => { 339 | configure( { ticketVersion: 3 } ); 340 | } ); 341 | 342 | describe( "when ticketVersion is missing", () => { 343 | it( "should add the new default version", () => { 344 | test( {}, { ticketVersion: 3 } ); 345 | } ); 346 | } ); 347 | 348 | describe( "when ticketVersion is wrong", () => { 349 | it( "should throw an error", () => { 350 | ( () => encrypt( { ticketVersion: 2 } ) ).should.throw( "Invalid ticket version 2, expected 3" ); 351 | } ); 352 | } ); 353 | 354 | describe( "when ticketVersion is correct", () => { 355 | it( "should use the set version", () => { 356 | const ticket = { ticketVersion: 3 }; 357 | test( ticket, ticket ); 358 | } ); 359 | } ); 360 | } ); 361 | 362 | describe( "with encryptAsBuffer set to true", () => { 363 | before( () => { 364 | configure( { encryptAsBuffer: true } ); 365 | } ); 366 | 367 | it( "should return a buffer object", () => { 368 | encrypt( {} ).should.be.an.instanceof( Buffer ); 369 | } ); 370 | } ); 371 | } ); 372 | --------------------------------------------------------------------------------