├── .travis.yml ├── .npmignore ├── sample ├── package.json ├── windows-graph-sample.js ├── cas.json ├── refresh-token-sample.js ├── client-credentials-sample.js ├── certificate-credentials-sample.js ├── username-password-sample.js ├── device-code-sample.js └── website-sample.js ├── .gitignore ├── .jshintrc ├── tsconfig.json ├── test ├── util │ └── cover ├── wstrust │ ├── RST2005.xml │ ├── RST.xml │ ├── common.base64.encoded.assertion.txt │ ├── common.rstr.xml │ ├── RSTR2005.xml │ └── RSTR.xml ├── log.ts ├── authorization-code.ts ├── user-realm.ts ├── self-signed-jwt.ts ├── mex.ts ├── acquire-user-code.ts ├── refresh-token.ts ├── wstrust-response.ts ├── wstrust-request.ts └── authority.ts ├── package.json ├── lib ├── adal.js ├── argument.js ├── memory-cache.js ├── code-request.js ├── xmlutil.js ├── self-signed-jwt.js ├── util.js ├── constants.js ├── log.js ├── user-realm.js ├── wstrust-response.js └── wstrust-request.js ├── changelog.md ├── RELEASES.md └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "8" -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/ 3 | tests/ 4 | .git** 5 | .vscode/ 6 | RELEASES.md 7 | sample/ 8 | tools/ 9 | tasks/ 10 | scripts/ 11 | .nuget/ 12 | packages/ 13 | packages.config 14 | .ntvs_analysis.* -------------------------------------------------------------------------------- /sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name" 3 | , "version": "0.0.1" 4 | , "private": true 5 | , "dependencies": { 6 | "@microsoft/microsoft-graph-client": "^1.0.0", 7 | "adal-node": ">= 0.1.3", 8 | "express": "3.x", 9 | "connect-logger": "0.x", 10 | "cookie-parser": "1.x", 11 | "cookie-session": "1.x" 12 | } 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | projects/* 2 | targets/* 3 | 4 | # Visual Studio # 5 | *.suo 6 | *.user 7 | /obj/ 8 | /bin/ 9 | .ntvs_analysis.dat 10 | 11 | # Node # 12 | test/recordings/ 13 | node_modules/ 14 | npm-debug.log 15 | azure_error 16 | checkstyle-result.xml 17 | test-result.xml 18 | lib-cov/ 19 | out/ 20 | 21 | # Mac OS # 22 | .DS_Store 23 | .DS_Store? 24 | 25 | # Windows # 26 | Thumbs.db 27 | 28 | # WebStorm # 29 | .idea/ 30 | 31 | # TS # 32 | test/**/*.js 33 | *.js.map 34 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "freeze": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "maxparams": false, 12 | "maxdepth": false, 13 | "maxstatements": false, 14 | "maxcomplexity": false, 15 | "multistr" : true, 16 | "newcap": true, 17 | "noarg": true, 18 | "node": true, 19 | "noempty": true, 20 | "nonew": true, 21 | "plusplus": false, 22 | "quotmark": "single", 23 | "regexp": true, 24 | "sub": true, 25 | "strict": true, 26 | "trailing": true, 27 | "undef": true, 28 | "unused": true 29 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "module": "commonjs", 5 | "noImplicitAny": true, 6 | "preserveConstEnums": true, 7 | "sourceMap": true, 8 | "newLine": "LF", 9 | "target": "es6", 10 | "moduleResolution": "node", 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "allowJs": false, 16 | "strict": true, 17 | "strictNullChecks": true, 18 | "lib": [ 19 | "dom", 20 | "dom.iterable", 21 | "es5", 22 | "es6", 23 | "es7", 24 | "esnext", 25 | "esnext.asynciterable", 26 | "es2015.iterable" 27 | ] 28 | }, 29 | "compileOnSave": true, 30 | "exclude": [ 31 | "node_modules" 32 | ], 33 | "include": [ 34 | "./lib/adal.d.ts", 35 | "./test" 36 | ] 37 | } -------------------------------------------------------------------------------- /test/util/cover: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LIB_DIR="lib" 4 | COVER_DIR=$LIB_DIR"-cov" 5 | 6 | if [ -z $ADAL_COV_INTERACTIVE ]; then 7 | REPORTER="JSON-cov" 8 | COVERAGE_FILENAME="cov.json" 9 | else 10 | REPORTER="HTML-cov" 11 | COVERAGE_FILENAME="cov.html" 12 | fi 13 | 14 | if [ -d lib-cov ]; then 15 | rm -r $COVER_DIR 16 | fi 17 | 18 | echo "Instrumenting..." 19 | jscoverage lib lib-cov 20 | if [ $? -ne 0 ]; then 21 | echo "jscoverage failed" 22 | exit 1 23 | fi 24 | 25 | echo "Running tests..." 26 | export ADAL_COV="TRUE" 27 | mocha -R $REPORTER --ui tdd test > $COVER_DIR/$COVERAGE_FILENAME 28 | if [ $? -ne 0 ]; then 29 | echo "mocha test run failed" 30 | exit 1 31 | fi 32 | 33 | unset ADAL_COV 34 | 35 | UNAME=`uname` 36 | if [ "$UNAME" = "Darwin" ]; then 37 | IS_MAC="true" 38 | fi 39 | 40 | if [[ $ADAL_COV_INTERACTIVE && $IS_MAC ]]; then 41 | echo "Starting safari..." 42 | open -a safari.app $COVER_DIR/$COVERAGE_FILENAME 43 | fi 44 | -------------------------------------------------------------------------------- /sample/windows-graph-sample.js: -------------------------------------------------------------------------------- 1 | const AuthenticationContext = require('adal-node').AuthenticationContext; 2 | const MicrosoftGraph = require("@microsoft/microsoft-graph-client"); 3 | 4 | const authorityHostUrl = 'https://login.windows.net'; 5 | const tenantName = ''; //azure active directory tenant name. ie: name.onmicrosoft.com 6 | const authorityUrl = authorityHostUrl + '/' + tenantName; 7 | const applicationId = ''; //application id for registered app 8 | const clientSecret = ''; //azure active directory registered app secret 9 | const resource = "https://graph.microsoft.com"; //URI of resource where token is valid 10 | 11 | const context = new AuthenticationContext(authorityUrl); 12 | 13 | context.acquireTokenWithClientCredentials( 14 | resource, 15 | applicationId, 16 | clientSecret, 17 | function(err, tokenResponse) { 18 | if (err) { 19 | console.log('well that didn\'t work: ' + err.stack); 20 | } else { 21 | let client = MicrosoftGraph.Client.init({ 22 | defaultVersion: 'v1.0', 23 | authProvider: (done) => { 24 | done(null, tokenResponse.accessToken); 25 | }, 26 | }); 27 | 28 | client 29 | .api('/users') 30 | .get((err, result) => { 31 | console.log(result, err); 32 | }); 33 | } 34 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "adal-node", 3 | "author": { 4 | "name": "Microsoft Open Technologies Inc", 5 | "email": "msopentech@microsoft.com", 6 | "url": "http://msopentech.com/" 7 | }, 8 | "license": "Apache-2.0", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/AzureAD/azure-activedirectory-library-for-nodejs.git" 12 | }, 13 | "version": "0.2.2", 14 | "description": "Windows Azure Active Directory Client Library for node", 15 | "keywords": [ 16 | "node", 17 | "azure", 18 | "AAD", 19 | "adal", 20 | "adfs", 21 | "oauth" 22 | ], 23 | "main": "./lib/adal.js", 24 | "types": "./lib/adal.d.ts", 25 | "engines": { 26 | "node": ">= 0.6.15" 27 | }, 28 | "dependencies": { 29 | "@types/node": "^8.0.47", 30 | "async": "^2.6.3", 31 | "date-utils": "*", 32 | "jws": "3.x.x", 33 | "underscore": ">= 1.3.1", 34 | "uuid": "^3.1.0", 35 | "xmldom": ">= 0.1.x", 36 | "xpath.js": "~1.1.0", 37 | "axios": "^0.21.1" 38 | }, 39 | "devDependencies": { 40 | "@types/mocha": "^2.2.44", 41 | "@types/nock": "^8.2.1", 42 | "@types/sinon": "^2.3.7", 43 | "@types/underscore": "^1.8.4", 44 | "jshint": "^2.10.2", 45 | "mocha": "*", 46 | "nock": "^10.0.6", 47 | "sinon": "^7.3.2", 48 | "typescript": "^2.6.1" 49 | }, 50 | "scripts": { 51 | "test": "npm run tsc && mocha -R spec --ui tdd test", 52 | "cover": "./test/util/cover", 53 | "doc": "jsdoc lib", 54 | "tsc": "tsc -p tsconfig.json" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/wstrust/RST2005.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue 4 | %MESSAGE_ID% 5 | 6 | http://www.w3.org/2005/08/addressing/anonymous 7 | 8 | %WSTRUST_ENDPOINT% 9 | 10 | 11 | %CREATED% 12 | %EXPIRES% 13 | 14 | 15 | %USERNAME% 16 | %PASSWORD% 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | %APPLIES_TO% 25 | 26 | 27 | http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey 28 | http://schemas.xmlsoap.org/ws/2005/02/trust/Issue 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/wstrust/RST.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue 4 | %MESSAGE_ID% 5 | 6 | http://www.w3.org/2005/08/addressing/anonymous 7 | 8 | %WSTRUST_ENDPOINT% 9 | 10 | 11 | %CREATED% 12 | %EXPIRES% 13 | 14 | 15 | %USERNAME% 16 | %PASSWORD% 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | %APPLIES_TO% 25 | 26 | 27 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer 28 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /lib/adal.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var _ = require('underscore'); 24 | 25 | var ac = require('./authentication-context'); 26 | var authParams = require('./authentication-parameters'); 27 | var logging = require('./log'); 28 | var MemoryCache = require('./memory-cache'); 29 | 30 | exports = {}; 31 | 32 | exports.Logging = logging.Logging; 33 | exports.AuthenticationContext = ac.AuthenticationContext; 34 | exports.setGlobalADALOptions = ac.setGlobalADALOptions; 35 | exports.getGlobalADALOptions = ac.getGlobalADALOptions; 36 | exports.MemoryCache = MemoryCache; 37 | _.extend(exports, authParams); 38 | 39 | /** 40 | * Creates a new AuthenticationContext object. By default the authority will be checked against 41 | * a list of known Azure Active Directory authorities. If the authority is not recognized as 42 | * one of these well known authorities then token acquisition will fail. This behavior can be 43 | * turned off via the validateAuthority parameter below. 44 | * @function 45 | * @param {string} authority A URL that identifies a token authority. 46 | * @param {bool} [validateAuthority] Turns authority validation on or off. This parameter default to true. 47 | * @returns {AuthenticationContext} A new authentication context. 48 | */ 49 | exports.createAuthenticationContext = function(authority, validateAuthority) { 50 | return new ac.AuthenticationContext(authority, validateAuthority); 51 | }; 52 | 53 | module.exports = exports; 54 | -------------------------------------------------------------------------------- /test/log.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 22 | 'use strict'; 23 | /* Directive tells jshint that suite and test are globals defined by mocha */ 24 | /* global suite */ 25 | /* global test */ 26 | 27 | import * as _ from "underscore"; 28 | import * as assert from "assert"; 29 | 30 | const util = require('./util/util'); 31 | 32 | import * as adal from "../lib/adal"; 33 | 34 | suite('log', function() { 35 | test('settings-none', function(done) { 36 | var currentOptions = adal.Logging.getLoggingOptions(); 37 | adal.Logging.setLoggingOptions(currentOptions); 38 | var options = adal.Logging.getLoggingOptions(); 39 | 40 | var noOptionsSet = (!options || 41 | null === options || 42 | {} === options || 43 | 0 === options.level); 44 | 45 | // Set the looging options back to what they were before this test so that 46 | // future tests are logged as they should be. 47 | adal.Logging.setLoggingOptions(currentOptions); 48 | 49 | assert(noOptionsSet, 'Did not expect to find any logging options set: ' + JSON.stringify(options)); 50 | done(); 51 | }); 52 | 53 | test('console-settings', function(done) { 54 | var currentOptions = adal.Logging.getLoggingOptions(); 55 | util.turnOnLogging(); 56 | var options = adal.Logging.getLoggingOptions(); 57 | var level = options.level; 58 | var logFunc = options.log; 59 | 60 | // Set the looging options back to what they were before this test so that 61 | // future tests are logged as they should be. 62 | adal.Logging.setLoggingOptions(currentOptions); 63 | 64 | assert(level === 3, `Logging level was not the expected value of 3: ${options.level}`); 65 | assert(_.isFunction(logFunc), 'Unexpected logging function: ' + logFunc); 66 | done(); 67 | }); 68 | }); 69 | 70 | -------------------------------------------------------------------------------- /sample/cas.json: -------------------------------------------------------------------------------- 1 | ["-----BEGIN CERTIFICATE-----\nMIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ\nRTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD\nVQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX\nDTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y\nZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy\nVHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr\nmD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr\nIZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK\nmpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu\nXmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy\ndc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye\njl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1\nBE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\nDQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92\n9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx\njkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0\nEpn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz\nksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS\nR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp\n-----END CERTIFICATE-----\n","Bag Attributes\n localKeyID: 01 00 00 00 \nsubject=/DC=com/DC=naturalcauses/CN=naturalcauses-RRANDALL-DC-CA\nissuer=/DC=com/DC=naturalcauses/CN=naturalcauses-RRANDALL-DC-CA\n-----BEGIN CERTIFICATE-----\nMIIDkzCCAnugAwIBAgIQaPPiByjAYpBIIsO71RJ3MDANBgkqhkiG9w0BAQsFADBb\nMRMwEQYKCZImiZPyLGQBGRYDY29tMR0wGwYKCZImiZPyLGQBGRYNbmF0dXJhbGNh\ndXNlczElMCMGA1UEAxMcbmF0dXJhbGNhdXNlcy1SUkFOREFMTC1EQy1DQTAgFw0x\nMzExMTAwMTMxMTZaGA8yMTEyMTExMDAxNDExNVowWzETMBEGCgmSJomT8ixkARkW\nA2NvbTEdMBsGCgmSJomT8ixkARkWDW5hdHVyYWxjYXVzZXMxJTAjBgNVBAMTHG5h\ndHVyYWxjYXVzZXMtUlJBTkRBTEwtREMtQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQC1UDXn2SWXpqr5C/YnsvlbYYyL09VH3L8EhO//wfUTmg4EGTh9\n0TbE4ypV43JInQBD7QRb8E1iotmeMUtPYXVCZovKPfiGn9H2TYLfZHWK3Rw7LtoJ\n9oraLOmtr3aiOCguYqHWssxABqq0GwChBx/qnVlhs1ijVOjsSKauW0+rirChevN4\nMn5YVdDCqFrjGEY+2Rxrp37Gbu0rBoZ1VpWPO5KDLIjuVu/dmNBYEOGD9xdCUiK6\nVGVjeQrRPMOdZApIi/vN+bwD7pEKP+Wr5H/tabLlBy7hyhpDoqAan66HgSDfRBw3\nQ/0xW+5xzsQE9qt3ge49m4Ez2fepmSIOcSRxAgMBAAGjUTBPMAsGA1UdDwQEAwIB\nhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQEFzsV9wru4PhtVtPMCbKGqbry\n4DAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQsFAAOCAQEAQtTt5nPbsYc0\nySA/QjbG9y45jY8iUw8cBDHwZgOkjJEn57OvGt4SbP2ZMnfdBISuLZJgx9fZcKVj\nc1BShZbhOaJWR99syW1JCYH1ibe6YpBCLOuZ+3xrAzm1OfB/Lj/5xSXfG+R6amlT\nz6iugDN5RKgIG3DONXwqzoCvqjMYFpfWb4Md5Wss0rf8K/Un0Y6CaIyB7C+pEzXS\nusCTisS6ipebJe9uw7aGomX1xyQFvyr2W74+o4sl8WpT4EFLSY00H1Rm55xcTjdl\nUu+GV9e+NlpQcByulwjImSWePI+mOPavH88rU9FntcRuyE3sK23kxD4OswqfY1w2\nYl3KFoepmA==\n-----END CERTIFICATE-----\n"] -------------------------------------------------------------------------------- /lib/argument.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var _ = require('underscore'); 24 | var constants = require('./constants'); 25 | 26 | var UserCodeResponseFields = constants.UserCodeResponseFields; 27 | 28 | var argumentValidation = { 29 | /** 30 | * Throws if the passed in parameter is not a string. 31 | * @param {string} param The parameter to validate. 32 | * @param {string} name The name of the parameter being validated. 33 | * @throws {Error} If the parameter is not a valid string. 34 | */ 35 | validateStringParameter : function(param, name) { 36 | if (!param) { 37 | throw new Error('The ' + name + ' parameter is required.'); 38 | } 39 | if (!_.isString(param)) { 40 | throw new Error('The ' + name + ' parameter must be of type String.'); 41 | } 42 | }, 43 | 44 | /** 45 | * Validates that the callback passed in {@link AuthenticationContext.acquireToken} is a function 46 | * @param {AcquireTokenCallback} callback 47 | * @throws {Error} If the callback parameter is not a function 48 | */ 49 | validateCallbackType : function(callback) { 50 | if (!callback || !_.isFunction(callback)) { 51 | throw new Error('acquireToken requires a function callback parameter.'); 52 | } 53 | }, 54 | 55 | validateUserCodeInfo : function(userCodeInfo) { 56 | if (!userCodeInfo){ 57 | throw new Error('The userCodeInfo parameter is required'); 58 | } 59 | 60 | if (!userCodeInfo.hasOwnProperty(UserCodeResponseFields.DEVICE_CODE)){ 61 | throw new Error('The userCodeInfo is missing device_code'); 62 | } 63 | 64 | if (!userCodeInfo.hasOwnProperty(UserCodeResponseFields.INTERVAL)){ 65 | throw new Error('The userCodeInfo is missing interval'); 66 | } 67 | 68 | if (!userCodeInfo.hasOwnProperty(UserCodeResponseFields.EXPIRES_IN)){ 69 | throw new Error('The userCodeInfo is missing expires_in'); 70 | } 71 | } 72 | }; 73 | 74 | module.exports = argumentValidation; 75 | -------------------------------------------------------------------------------- /sample/refresh-token-sample.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var adal = require('adal-node'); 4 | var fs = require('fs'); 5 | var https = require('https'); 6 | 7 | var AuthenticationContext = adal.AuthenticationContext; 8 | 9 | function turnOnLogging() { 10 | var log = adal.Logging; 11 | log.setLoggingOptions( 12 | { 13 | level : log.LOGGING_LEVEL.VERBOSE, 14 | log : function(level, message, error) { 15 | console.log(message); 16 | if (error) { 17 | console.log(error); 18 | } 19 | } 20 | }); 21 | } 22 | 23 | turnOnLogging(); 24 | 25 | 26 | /* 27 | * You can override the default account information by providing a JSON file 28 | * with the same parameters as the sampleParameters variable below. Either 29 | * through a command line argument, 'node sample.js parameters.json', or 30 | * specifying in an environment variable. 31 | * { 32 | * "tenant" : "rrandallaad1.onmicrosoft.com", 33 | * "authorityHostUrl" : "https://login.windows.net", 34 | * "clientId" : "624ac9bd-4c1c-4687-aec8-b56a8991cfb3", 35 | * "username" : "user1", 36 | * "password" : "verySecurePassword" 37 | * } 38 | */ 39 | var parametersFile = process.argv[2] || process.env['ADAL_SAMPLE_PARAMETERS_FILE']; 40 | 41 | var sampleParameters; 42 | if (parametersFile) { 43 | var jsonFile = fs.readFileSync(parametersFile); 44 | if (jsonFile) { 45 | sampleParameters = JSON.parse(jsonFile); 46 | } else { 47 | console.log('File not found, falling back to defaults: ' + parametersFile); 48 | } 49 | } 50 | 51 | if (!parametersFile) { 52 | sampleParameters = { 53 | tenant : 'rrandallaad1.onmicrosoft.com', 54 | authorityHostUrl : 'https://login.windows.net', 55 | clientId : '624ac9bd-4c1c-4686-aec8-b56a8991cfb3', 56 | username : 'frizzo@naturalcauses.com', 57 | password : '' 58 | }; 59 | } 60 | 61 | var authorityUrl = sampleParameters.authorityHostUrl + '/' + sampleParameters.tenant; 62 | 63 | var resource = '00000002-0000-0000-c000-000000000000'; 64 | 65 | var casjson = fs.readFileSync('./cas.json'); 66 | var cas = JSON.parse(casjson); 67 | 68 | https.globalAgent.options.ca = cas; 69 | 70 | 71 | var context = new AuthenticationContext(authorityUrl); 72 | 73 | context.acquireTokenWithUsernamePassword(resource, sampleParameters.username, sampleParameters.password, sampleParameters.clientId, function(err, tokenResponse) { 74 | if (err) { 75 | console.log('well that didn\'t work: ' + err.stack); 76 | return; 77 | } else { 78 | console.log(tokenResponse); 79 | console.log('\nRefreshing token.\n'); 80 | 81 | context.acquireTokenWithRefreshToken(tokenResponse['refreshToken'], sampleParameters.clientId, null, function(err, tokenResponse) { 82 | if (err) { 83 | console.log('well refreshing didn\'t work: ' + err.stack); 84 | return; 85 | } else { 86 | console.log(tokenResponse); 87 | } 88 | }); 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /sample/client-credentials-sample.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var fs = require('fs'); 24 | var adal = require('adal-node'); 25 | 26 | 27 | var AuthenticationContext = adal.AuthenticationContext; 28 | 29 | function turnOnLogging() { 30 | var log = adal.Logging; 31 | log.setLoggingOptions( 32 | { 33 | level : log.LOGGING_LEVEL.VERBOSE, 34 | log : function(level, message, error) { 35 | console.log(message); 36 | if (error) { 37 | console.log(error); 38 | } 39 | } 40 | }); 41 | } 42 | 43 | /* 44 | * You can override the default account information by providing a JSON file 45 | * with the same parameters as the sampleParameters variable below. Either 46 | * through a command line argument, 'node sample.js parameters.json', or 47 | * specifying in an environment variable. 48 | * { 49 | * "tenant" : "rrandallaad1.onmicrosoft.com", 50 | * "authorityHostUrl" : "https://login.windows.net", 51 | * "clientId" : "624ac9bd-4c1c-4687-aec8-b56a8991cfb3", 52 | * "clientSecret" : "verySecret="" 53 | * } 54 | */ 55 | var parametersFile = process.argv[2] || process.env['ADAL_SAMPLE_PARAMETERS_FILE']; 56 | 57 | var sampleParameters; 58 | if (parametersFile) { 59 | var jsonFile = fs.readFileSync(parametersFile); 60 | if (jsonFile) { 61 | sampleParameters = JSON.parse(jsonFile); 62 | } else { 63 | console.log('File not found, falling back to defaults: ' + parametersFile); 64 | } 65 | } 66 | 67 | if (!parametersFile) { 68 | sampleParameters = { 69 | tenant : 'rrandallaad1.onmicrosoft.com', 70 | authorityHostUrl : 'https://login.windows.net', 71 | clientId : '624ac9bd-4c1c-4687-aec8-b56a8991cfb3', 72 | clientSecret : '' 73 | }; 74 | } 75 | 76 | var authorityUrl = sampleParameters.authorityHostUrl + '/' + sampleParameters.tenant; 77 | 78 | var resource = '00000002-0000-0000-c000-000000000000'; 79 | 80 | turnOnLogging(); 81 | 82 | var context = new AuthenticationContext(authorityUrl); 83 | 84 | context.acquireTokenWithClientCredentials(resource, sampleParameters.clientId, sampleParameters.clientSecret, function(err, tokenResponse) { 85 | if (err) { 86 | console.log('well that didn\'t work: ' + err.stack); 87 | } else { 88 | console.log(tokenResponse); 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /lib/memory-cache.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 22 | 'use strict'; 23 | 24 | var _ = require('underscore'); 25 | 26 | /** 27 | * Constructs a new in memory token cache. 28 | * @constructor 29 | */ 30 | function MemoryCache() { 31 | this._entries = []; 32 | } 33 | 34 | /** 35 | * Removes a collection of entries from the cache in a single batch operation. 36 | * @param {Array} entries An array of cache entries to remove. 37 | * @param {Function} callback This function is called when the operation is complete. Any error is provided as the 38 | * first parameter. 39 | */ 40 | MemoryCache.prototype.remove = function(entries, callback) { 41 | var updatedEntries = _.filter(this._entries, function(element) { 42 | if (_.findWhere(entries, element)) { 43 | return false; 44 | } 45 | return true; 46 | }); 47 | 48 | this._entries = updatedEntries; 49 | callback(); 50 | }; 51 | 52 | /** 53 | * Adds a collection of entries to the cache in a single batch operation. 54 | * @param {Array} entries An array of entries to add to the cache. 55 | * @param {Function} callback This function is called when the operation is complete. Any error is provided as the 56 | * first parameter. 57 | */ 58 | MemoryCache.prototype.add = function(entries, callback) { 59 | // Remove any entries that are duplicates of the existing 60 | // cache elements. 61 | _.each(this._entries, function(element) { 62 | _.each(entries, function(addElement, index) { 63 | if (_.isEqual(element, addElement)) { 64 | entries[index] = null; 65 | } 66 | }); 67 | }); 68 | 69 | // Add the new entries to the end of the cache. 70 | entries = _.compact(entries); 71 | for (var i = 0; i < entries.length; i++) { 72 | this._entries.push(entries[i]); 73 | } 74 | 75 | callback(null, true); 76 | }; 77 | 78 | /** 79 | * Finds all entries in the cache that match all of the passed in values. 80 | * @param {object} query This object will be compared to each entry in the cache. Any entries that 81 | * match all of the values in this object will be returned. All the values 82 | * in the passed in object must match values in a potentialy returned object 83 | * exactly. The returned object may have more values than the passed in query 84 | * object. 85 | * @param {TokenCacheFindCallback} callback 86 | */ 87 | MemoryCache.prototype.find = function(query, callback) { 88 | var results = _.where(this._entries, query); 89 | callback(null, results); 90 | }; 91 | 92 | module.exports = MemoryCache; -------------------------------------------------------------------------------- /sample/certificate-credentials-sample.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var fs = require('fs'); 24 | var adal = require('adal-node'); 25 | 26 | 27 | var AuthenticationContext = adal.AuthenticationContext; 28 | 29 | function turnOnLogging() { 30 | var log = adal.Logging; 31 | log.setLoggingOptions( 32 | { 33 | level : log.LOGGING_LEVEL.VERBOSE, 34 | log : function(level, message, error) { 35 | console.log(message); 36 | if (error) { 37 | console.log(error); 38 | } 39 | } 40 | }); 41 | } 42 | 43 | function getPrivateKey(filename) { 44 | var privatePem = fs.readFileSync(filename, { encoding : 'utf8'}); 45 | return privatePem; 46 | } 47 | 48 | /* 49 | * You can override the default account information by providing a JSON file 50 | * with the same parameters as the sampleParameters variable below. Either 51 | * through a command line argument, 'node sample.js parameters.json', or 52 | * specifying in an environment variable. 53 | * privateKeyFile must contain a PEM encoded cert with private key. 54 | * thumbprint must be the thumbprint of the privateKeyFile. 55 | * { 56 | * tenant : 'naturalcauses.onmicrosoft.com', 57 | * authorityHostUrl : 'https://login.windows.net', 58 | * clientId : 'd6835713-b745-48d1-bb62-7a8248477d35', 59 | * thumbprint : 'C1:5D:EA:86:56:AD:DF:67:BE:80:31:D8:5E:BD:DC:5A:D6:C4:36:E1', 60 | * privateKeyFile : 'ncwebCTKey.pem' 61 | * } 62 | */ 63 | var parametersFile = process.argv[2] || process.env['ADAL_SAMPLE_PARAMETERS_FILE']; 64 | 65 | var sampleParameters; 66 | if (parametersFile) { 67 | var jsonFile = fs.readFileSync(parametersFile); 68 | if (jsonFile) { 69 | sampleParameters = JSON.parse(jsonFile); 70 | } else { 71 | console.log('File not found, falling back to defaults: ' + parametersFile); 72 | } 73 | } 74 | 75 | sampleParameters = { 76 | tenant : 'naturalcauses.com', 77 | authorityHostUrl : 'https://login.windows.net', 78 | clientId : 'd6835713-b745-48d1-bb62-7a8248477d35', 79 | thumbprint : 'C15DEA8656ADDF67BE8031D85EBDDC5AD6C436E1', 80 | privateKeyFile : '' 81 | }; 82 | 83 | var authorityUrl = sampleParameters.authorityHostUrl + '/' + sampleParameters.tenant; 84 | 85 | var resource = '00000002-0000-0000-c000-000000000000'; 86 | 87 | turnOnLogging(); 88 | 89 | var context = new AuthenticationContext(authorityUrl); 90 | var key = getPrivateKey(sampleParameters.privateKeyFile); 91 | 92 | context.acquireTokenWithClientCertificate(resource, sampleParameters.clientId, key, sampleParameters.thumbprint, function(err, tokenResponse) { 93 | if (err) { 94 | console.log('well that didn\'t work: ' + err.stack); 95 | } else { 96 | console.log(tokenResponse); 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /lib/code-request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var constants = require('./constants'); 24 | var Logger = require('./log').Logger; 25 | var Mex = require('./mex'); 26 | var OAuth2Client = require('./oauth2client'); 27 | 28 | var OAuth2Parameters = constants.OAuth2.Parameters; 29 | var TokenResponseFields = constants.TokenResponseFields; 30 | var OAuth2GrantType = constants.OAuth2.GrantType; 31 | var OAuth2Scope = constants.OAuth2.Scope; 32 | 33 | /** 34 | * Constructs a new CodeRequest object. 35 | * @constructor 36 | * @private 37 | * @param {object} callContext Contains any context information that applies to the request. 38 | * @param {AuthenticationContext} authenticationContext 39 | * @param {string} resource 40 | * @param {string} clientId 41 | */ 42 | // TODO: probably need to modify the parameter list. 43 | function CodeRequest(callContext, authenticationContext, clientId, resource) { 44 | this._log = new Logger('DeviceCodeRequest', callContext._logContext); 45 | this._callContext = callContext; 46 | this._authenticationContext = authenticationContext; 47 | this._resource = resource; 48 | this._clientId = clientId; 49 | 50 | // This should be set at the beginning of getToken 51 | // functions that have a userId. 52 | this._userId = null; 53 | }; 54 | 55 | /** 56 | * Get user code info. 57 | * @private 58 | * @param {object} oauthParameters containing all the parameters needed to get the user code info. 59 | * @param {callback} callback 60 | */ 61 | CodeRequest.prototype._getUserCodeInfo = function (oauthParameters, callback) { 62 | var oauth2Client = this._createOAuth2Client(); 63 | oauth2Client.getUserCodeInfo(oauthParameters, callback); 64 | }; 65 | 66 | CodeRequest.prototype._createOAuth2Client = function () { 67 | return new OAuth2Client(this._callContext, this._authenticationContext._authority); 68 | }; 69 | 70 | /** 71 | * Creates a set of basic, common, OAuthParameters based on values that the CodeRequest was created with. 72 | * @private 73 | * @return {object} containing all the basic parameters. 74 | */ 75 | CodeRequest.prototype._createOAuthParameters = function () { 76 | var oauthParameters = {}; 77 | 78 | oauthParameters[OAuth2Parameters.CLIENT_ID] = this._clientId; 79 | oauthParameters[OAuth2Parameters.RESOURCE] = this._resource; 80 | 81 | return oauthParameters; 82 | }; 83 | 84 | /** 85 | * Get the user code information. 86 | * @param {string} language optional parameter used to get the user code info. 87 | * @param {callback} callback 88 | */ 89 | CodeRequest.prototype.getUserCodeInfo = function(language, callback) { 90 | this._log.info('Getting user code info.'); 91 | 92 | var oauthParameters = this._createOAuthParameters(); 93 | if (language){ 94 | oauthParameters[OAuth2Parameters.LANGUAGE] = language; 95 | } 96 | 97 | this._getUserCodeInfo(oauthParameters, callback); 98 | }; 99 | module.exports = CodeRequest; -------------------------------------------------------------------------------- /sample/username-password-sample.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var adal = require('../lib/adal'); 24 | var fs = require('fs'); 25 | var https = require('https'); 26 | 27 | var AuthenticationContext = adal.AuthenticationContext; 28 | 29 | function turnOnLogging() { 30 | var log = adal.Logging; 31 | log.setLoggingOptions( 32 | { 33 | level : log.LOGGING_LEVEL.VERBOSE, 34 | log : function(level, message, error) { 35 | console.log(message); 36 | if (error) { 37 | console.log(error); 38 | } 39 | } 40 | }); 41 | } 42 | 43 | turnOnLogging(); 44 | 45 | /* 46 | * You can override the default account information by providing a JSON file 47 | * with the same parameters as the sampleParameters variable below. Either 48 | * through a command line argument, 'node sample.js parameters.json', or 49 | * specifying in an environment variable. 50 | * { 51 | * "tenant" : "rrandallaad1.onmicrosoft.com", 52 | * "authorityHostUrl" : "https://login.windows.net", 53 | * "clientId" : "624ac9bd-4c1c-4687-aec8-b56a8991cfb3", 54 | * "username" : "user1", 55 | * "password" : "verySecurePassword" 56 | * } 57 | */ 58 | var parametersFile = process.argv[2] || process.env['ADAL_SAMPLE_PARAMETERS_FILE']; 59 | 60 | var sampleParameters; 61 | if (parametersFile) { 62 | var jsonFile = fs.readFileSync(parametersFile); 63 | if (jsonFile) { 64 | sampleParameters = JSON.parse(jsonFile); 65 | } else { 66 | console.log('File not found, falling back to defaults: ' + parametersFile); 67 | } 68 | } 69 | 70 | if (!parametersFile) { 71 | sampleParameters = { 72 | tenant : 'rrandallaad1.onmicrosoft.com', 73 | authorityHostUrl : 'https://login.windows.net', 74 | clientId : 'd3590ed6-52b3-4102-aeff-aad2292ab01c', 75 | username : '', 76 | password : '' 77 | }; 78 | } 79 | 80 | var authorityUrl = sampleParameters.authorityHostUrl + '/' + sampleParameters.tenant; 81 | 82 | var resource = '00000002-0000-0000-c000-000000000000'; 83 | 84 | // +++++ Using a Private CA ++++++ 85 | // If you are testing against an ADFS instance whose SSL cert was issued by 86 | // a private CA then you need to tell node to trust that CA. The following 87 | // few lines of code does that. The cas.json file is an array of PEM files, 88 | // one per entry. Once you override one CA you must override them all. 89 | // As a result, the cas.json file contains a PEM file for each CA that 90 | // is trusted by node and mozilla by default, plus a perm file for the 91 | // private CA that the test adfs server fs.naturalcauses.com uses. Add your 92 | // own CA PEM file as an entry to this file if necessary. 93 | var casjson = fs.readFileSync('./cas.json'); 94 | var cas = JSON.parse(casjson); 95 | https.globalAgent.options.ca = cas; 96 | 97 | 98 | var context = new AuthenticationContext(authorityUrl); 99 | 100 | context.acquireTokenWithUsernamePassword(resource, sampleParameters.username, sampleParameters.password, sampleParameters.clientId, function(err, tokenResponse) { 101 | if (err) { 102 | console.log('well that didn\'t work: ' + err.stack); 103 | } else { 104 | console.log(tokenResponse); 105 | } 106 | }); 107 | -------------------------------------------------------------------------------- /test/authorization-code.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 22 | 'use strict'; 23 | /* Directive tells jshint that suite and test are globals defined by mocha */ 24 | /* global suite */ 25 | /* global test */ 26 | 27 | import * as assert from "assert"; 28 | import * as nock from "nock"; 29 | import * as querystring from "querystring"; 30 | 31 | import * as adal from "../lib/adal"; 32 | const util = require('./util/util'); 33 | const cp: any = util.commonParameters; 34 | var AuthenticationContext = adal.AuthenticationContext; 35 | 36 | /** 37 | * Tests AuthenticationContext.acquireTokenWithAuthorizationCode 38 | */ 39 | suite('authorization-code', function () { 40 | var authorizationCode = '1234870909'; 41 | var redirectUri = 'app_bundle:test.bar.baz'; 42 | 43 | function setupExpectedAuthCodeTokenRequestResponse(httpCode: number, returnDoc: object, authorityEndpoint?: string) { 44 | var authEndpoint = util.getNockAuthorityHost(authorityEndpoint); 45 | 46 | var queryParameters: any = {}; 47 | queryParameters['grant_type'] = 'authorization_code'; 48 | queryParameters['code'] = authorizationCode; 49 | queryParameters['client_id'] = cp.clientId; 50 | queryParameters['client_secret'] = cp.clientSecret; 51 | queryParameters['resource'] = cp.resource; 52 | queryParameters['redirect_uri'] = redirectUri; 53 | 54 | var query = querystring.stringify(queryParameters); 55 | 56 | var tokenRequest = nock(authEndpoint) 57 | .filteringRequestBody(function (body) { 58 | return util.filterQueryString(query, body); 59 | }) 60 | .post(cp.tokenUrlPath, query) 61 | .reply(httpCode, returnDoc); 62 | 63 | util.matchStandardRequestHeaders(tokenRequest); 64 | 65 | return tokenRequest; 66 | } 67 | 68 | test('happy-path', function (done) { 69 | var response = util.createResponse(); 70 | var tokenRequest = setupExpectedAuthCodeTokenRequestResponse(200, response.wireResponse); 71 | 72 | var context = new AuthenticationContext(cp.authUrl); 73 | context.acquireTokenWithAuthorizationCode(authorizationCode, redirectUri, response.resource, cp.clientId, cp.clientSecret, function (err, tokenResponse) { 74 | if (!err) { 75 | assert(util.isMatchTokenResponse(response.decodedResponse, tokenResponse), 'The response did not match what was expected'); 76 | tokenRequest.done(); 77 | } 78 | done(err); 79 | }); 80 | }); 81 | 82 | test('failed-http-request', function (done) { 83 | this.timeout(6000); 84 | this.slow(4000); // This test takes longer than I would like to fail. It probably needs a better way of producing this error. 85 | 86 | nock.enableNetConnect(); 87 | var context = new AuthenticationContext('https://0.1.1.1:12/my.tenant.com'); 88 | context.acquireTokenWithAuthorizationCode(authorizationCode, redirectUri, cp.resource, cp.clientId, cp.clientSecret, function (err) { 89 | assert(err, 'Did not receive expected error on failed http request.'); 90 | nock.disableNetConnect(); 91 | done(); 92 | }); 93 | }); 94 | 95 | test('bad-argument', function (done) { 96 | var context = new AuthenticationContext(cp.authUrl); 97 | context.acquireTokenWithAuthorizationCode(authorizationCode, redirectUri, null as any, cp.clientId, cp.clientSecret, function (err) { 98 | assert(err, 'Did not receive expected argument error.'); 99 | done(); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /sample/device-code-sample.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var fs = require('fs'); 24 | var adal = require('../lib/adal.js'); 25 | var async = require('async'); 26 | var url = require('url'); 27 | var MemoryCache = require('../lib/memory-cache'); 28 | 29 | var AuthenticationContext = adal.AuthenticationContext; 30 | 31 | function turnOnLogging() { 32 | var log = adal.Logging; 33 | log.setLoggingOptions( 34 | { 35 | level : log.LOGGING_LEVEL.VERBOSE, 36 | log : function (level, message, error) { 37 | console.log(message); 38 | if (error) { 39 | console.log(error); 40 | } 41 | } 42 | }); 43 | } 44 | 45 | /* 46 | * You can override the default account information by providing a JSON file 47 | * with the same parameters as the sampleParameters variable below. Either 48 | * through a command line argument, 'node sample.js parameters.json', or 49 | * specifying in an environment variable. 50 | * { 51 | * "tenant" : "rrandallaad1.onmicrosoft.com", 52 | * "authorityHostUrl" : "https://login.windows.net", 53 | * "clientId" : "624ac9bd-4c1c-4687-aec8-b56a8991cfb3", 54 | * "clientSecret" : "verySecret="" 55 | * } 56 | */ 57 | var parametersFile = process.argv[2] || process.env['ADAL_SAMPLE_PARAMETERS_FILE' ]; 58 | 59 | var sampleParameters; 60 | if (parametersFile) { 61 | var jsonFile = fs.readFileSync(parametersFile); 62 | if (jsonFile) { 63 | sampleParameters = JSON.parse(jsonFile); 64 | } else { 65 | console.log('File not found, falling back to defaults: ' + parametersFile); 66 | } 67 | } 68 | 69 | if (!parametersFile) { 70 | sampleParameters = { 71 | tenant : 'convergeTest.onmicrosoft.com', 72 | authorityHostUrl : 'https://login.microsoftonline.com', 73 | clientId : '', 74 | anothertenant: '' 75 | }; 76 | } 77 | 78 | var authorityUrl = sampleParameters.authorityHostUrl + '/' + sampleParameters.tenant; 79 | 80 | var resource = '00000002-0000-0000-c000-000000000000'; 81 | var userId = ''; 82 | 83 | //turnOnLogging(); 84 | 85 | var cache = new MemoryCache(); 86 | 87 | var context = new AuthenticationContext(authorityUrl, null, cache); 88 | context.acquireUserCode(resource, sampleParameters.clientId, 'es-mx', function (err, response) { 89 | if (err) { 90 | console.log('well that didn\'t work: ' + err.stack); 91 | } else { 92 | console.log(response); 93 | console.log('calling acquire token with device code'); 94 | context.acquireTokenWithDeviceCode(resource, sampleParameters.clientId, response, function (err, tokenResponse) { 95 | if (err) { 96 | console.log('error happens when acquiring token with device code'); 97 | console.log(err); 98 | } 99 | else { 100 | authorityUrl = sampleParameters.authorityHostUrl + '/' + sampleParameters.anothertenant; 101 | 102 | var context2 = new AuthenticationContext(authorityUrl, null, cache); 103 | context2.acquireToken(resource, userId, sampleParameters.clientId, function (err, tokenResponse) { 104 | if (err) { 105 | console.log('error happens when acquiring token with device code'); 106 | console.log(err); 107 | } 108 | else { 109 | console.log(tokenResponse); 110 | } 111 | }); 112 | } 113 | }); 114 | } 115 | }); 116 | 117 | -------------------------------------------------------------------------------- /lib/xmlutil.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var _ = require('underscore'); 24 | var select = require('xpath.js'); 25 | var XMLSerializer = require('xmldom').XMLSerializer; 26 | 27 | var constants = require('./constants'); 28 | 29 | /** 30 | * @namespace XmlUtil 31 | * @private 32 | */ 33 | 34 | var XPATH_PATH_TEMPLATE = '*[local-name() = \'LOCAL_NAME\' and namespace-uri() = \'NAMESPACE\']'; 35 | /** 36 | * The xpath implementation being used does not have a way of matching expanded namespace. 37 | * This method takes an xpath query and expands all of the namespaces involved. It then 38 | * re-writes the query in to a longer form that directory matches the correct namespaces. 39 | * @private 40 | * @static 41 | * @memberOf XmlUtil 42 | * @param {string} xpath The expath query string to expand. 43 | * @returns {string} An expanded xpath query. 44 | */ 45 | function expandQNames(xpath) { 46 | var namespaces = constants.XmlNamespaces; 47 | var pathParts = xpath.split('/'); 48 | for (var i=0; i < pathParts.length; i++) { 49 | if (pathParts[i].indexOf(':') !== -1) { 50 | var QNameParts = pathParts[i].split(':'); 51 | if (QNameParts.length !== 2) { 52 | throw new Error('Unable to parse XPath string : ' + xpath + ' : with QName : ' + pathParts[i]); 53 | } 54 | var expandedPath = XPATH_PATH_TEMPLATE.replace('LOCAL_NAME', QNameParts[1]); 55 | expandedPath = expandedPath.replace('NAMESPACE', namespaces[QNameParts[0]]); 56 | pathParts[i] = expandedPath; 57 | } 58 | } 59 | return pathParts.join('/'); 60 | } 61 | 62 | var exports = { 63 | 64 | /** 65 | * Performs an xpath select that does appropriate namespace matching since the imported 66 | * xpath module does not properly handle namespaces. 67 | * @static 68 | * @memberOf XmlUtil 69 | * @param {object} dom A dom object created by the xmldom module 70 | * @param {string} xpath An xpath expression 71 | * @return {array} An array of matching dom nodes. 72 | */ 73 | xpathSelect : function (dom, xpath) { 74 | return select(dom, expandQNames(xpath)); 75 | }, 76 | 77 | /** 78 | * Given a dom node serializes all immediate children that are xml elements. 79 | * @static 80 | * @memberOf XmlUtil 81 | * @param {object} node An xml dom node. 82 | * @return {string} Serialized xml. 83 | */ 84 | serializeNodeChildren : function(node) { 85 | var doc = ''; 86 | var sibling = node.firstChild; 87 | var serializer = new XMLSerializer(); 88 | 89 | while (sibling) { 90 | if (this.isElementNode(sibling)) { 91 | doc += serializer.serializeToString(sibling); 92 | } 93 | sibling = sibling.nextSibling; 94 | } 95 | 96 | return doc !== '' ? doc : null; 97 | }, 98 | 99 | /** 100 | * Detects whether the passed in dom node represents an xml element. 101 | * @static 102 | * @memberOf XmlUtil 103 | * @param {object} node An xml dom node. 104 | * @return {Boolean} true if the node represents an element. 105 | */ 106 | isElementNode : function(node) { 107 | return _.has(node, 'tagName'); 108 | }, 109 | 110 | /** 111 | * Given an xmldom node this function returns any text data contained within. 112 | * @static 113 | * @memberOf XmlUtil 114 | * @param {object} node An xmldom node from which the data should be extracted. 115 | * @return {string} Any data found within the element or null if none is found. 116 | */ 117 | findElementText : function(node) { 118 | var sibling = node.firstChild; 119 | while (sibling && !sibling.data) { 120 | sibling = sibling.nextSibling; 121 | } 122 | 123 | return sibling.data ? sibling.data : null; 124 | } 125 | }; 126 | 127 | module.exports = exports; -------------------------------------------------------------------------------- /test/user-realm.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 22 | 'use strict'; 23 | /* Directive tells jshint that suite and test are globals defined by mocha */ 24 | /* global suite */ 25 | /* global test */ 26 | import * as assert from "assert"; 27 | import * as nock from "nock"; 28 | 29 | const util = require('./util/util'); 30 | const cp = util.commonParameters; 31 | const testRequire = util.testRequire; 32 | 33 | const UserRealm = testRequire('user-realm'); 34 | 35 | /** 36 | * Tests the UserRealm class and user realm discovery. 37 | */ 38 | suite('UserRealm', function() { 39 | var authority = 'https://login.windows.net/'; 40 | var user = 'test@federatedtenant-com'; 41 | 42 | function setupExpectedResponse(doc: any) { 43 | var userRealmPath = cp.userRealmPathTemplate.replace('', encodeURIComponent(user)); 44 | var query = 'api-version=1.0'; 45 | 46 | var userRealmRequest = nock(authority) 47 | .filteringPath(function(path) { 48 | return util.removeQueryStringIfMatching(path, query); 49 | }) 50 | .get(userRealmPath) 51 | .reply(200, doc); 52 | 53 | util.matchStandardRequestHeaders(userRealmRequest); 54 | 55 | return userRealmRequest; 56 | } 57 | 58 | function negativeTest(response: any, done: Function) { 59 | var userRealmRequest = setupExpectedResponse(response); 60 | 61 | var userRealm = new UserRealm(cp.callContext, user, authority); 62 | userRealm.discover(function(err: Error) { 63 | userRealmRequest.done(); 64 | assert(err, 'Did not receive expected error'); 65 | done(); 66 | }); 67 | } 68 | 69 | test('happy-path-federated', function(done) { 70 | var userRealmResponse = '{\"account_type\":\"Federated\",\"federation_protocol\":\"wstrust\",\"federation_metadata_url\":\"https://adfs.federatedtenant.com/adfs/services/trust/mex\",\"federation_active_auth_url\":\"https://adfs.federatedtenant.com/adfs/services/trust/2005/usernamemixed\",\"ver\":\"0.8\"}'; 71 | var userRealmRequest = setupExpectedResponse(userRealmResponse); 72 | 73 | var userRealm = new UserRealm(cp.callContext, user, authority); 74 | userRealm.discover(function(err: Error) { 75 | userRealmRequest.done(); 76 | if (!err) { 77 | assert(userRealm.federationMetadataUrl === 'https://adfs.federatedtenant.com/adfs/services/trust/mex', 78 | 'Returned Mex URL does not match expected value:' + userRealm.federationMetadataUrl); 79 | assert(userRealm.federationActiveAuthUrl === 'https://adfs.federatedtenant.com/adfs/services/trust/2005/usernamemixed', 80 | 'Returned active auth URL does not match expected value: ' + userRealm.federationActiveAuthUrl); 81 | } 82 | done(err); 83 | }); 84 | }); 85 | 86 | test('negative-wrong-field', function(done) { 87 | var response = '{\"account_type\":\"Manageddf\",\"federation_protocol\":\"SAML20fgfg\",\"federation_metadata\":\"https://adfs.federatedtenant.com/adfs/services/trust/mex\",\"federation_active_auth_url\":\"https://adfs.federatedtenant.com/adfs/services/trust/2005/usernamemixed\",\"version\":\"0.8\"}'; 88 | negativeTest(response, done); 89 | }); 90 | 91 | test('negative-noroot', function(done) { 92 | var response = 'noroot'; 93 | negativeTest(response, done); 94 | }); 95 | 96 | test('negative-empty-json', function(done) { 97 | var response = '{}'; 98 | negativeTest(response, done); 99 | }); 100 | 101 | test('negative-fed-err', function(done) { 102 | var response = '{\"account_type\":\"Federated\",\"federation_protocol\":\"wstrustww\",\"federation_metadata_url\":\"https://adfs.federatedtenant.com/adfs/services/trust/mex\",\"federation_active_auth_url\":\"https://adfs.federatedtenant.com/adfs/services/trust/2005/usernamemixed\",\"ver\":\"0.8\"}'; 103 | negativeTest(response, done); 104 | }); 105 | }); 106 | 107 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | Version 0.2.2 2 | ------------------ 3 | Release Date: 8 February 2020 4 | * Replace 'request` with `axios` final version 5 | * Fix unit tests 6 | 7 | Version 0.2.2-beta 8 | ------------------ 9 | Release Date: 1 February 2020 10 | * Replace 'request` with `axios` 11 | 12 | Version 0.2.0 13 | ------------------ 14 | Release Date: 23 August 2019 15 | * Add support for user provided api version 16 | 17 | Version 0.1.28 18 | -------------- 19 | Release Date: 26 Feburary 2018 20 | * Added GDPR support per Microsoft policy. 21 | 22 | Version 0.1.27 23 | -------------- 24 | Release Date: 08 January 2018 25 | * Fix Issue #185 Dependency "xpath.js" version does not have an OSI-approved license 26 | 27 | Version 0.1.26 28 | -------------- 29 | Release Date: 05 December 2017 30 | * Added .npmignore to avoid publishing unnecessary files 31 | 32 | Version 0.1.25 33 | -------------- 34 | Release Date: 06 November 2017 35 | * Fixed typing: acquireUserCode returns UserCodeInfo in the callback 36 | 37 | Version 0.1.24 38 | -------------- 39 | Release Date: 06 November 2017 40 | * Added type definitions to the project. This should help users developing their apps in TypeScript #142, [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped/issues/12586). 41 | * Replaced `"node-uuid"` with `"uuid"` #170. 42 | 43 | Version 0.1.23 44 | -------------- 45 | Release Date: 26 Oct 2017 46 | * Fix Issue #95 - Change npm licenses to license and use spdx syntax 47 | * Fix Issue #122 - Broken link on README file 48 | * Fix Issue #155 - remove need for dynamic access to package.json to obtain lib version (util.js) 49 | * Fix Issue #172 - Add `login.microsoftonline.us` endpoint 50 | * Added Windows Graph Sample 51 | 52 | Version 0.1.22 53 | -------------- 54 | Release Date: 1 Aug 2016 55 | * Fix Issue #132 - Trim developer provided authority to construct endpoints 56 | 57 | Version 0.1.21 58 | -------------- 59 | Release Date: 23 Jun 2016 60 | * Policheck and credscan fixes 61 | 62 | Version 0.1.20 63 | -------------- 64 | Release Date: 17 Jun 2016 65 | * Add support for resource owner grant flow for ADFS 66 | 67 | Version 0.1.19 68 | -------------- 69 | Release Date: 26 Apr 2016 70 | * Fixed CredScan issue for the checked in pem file 71 | * Policheck fixes 72 | * Updated node-uuid 73 | * Fixed Readme.md 74 | * Inline the adal version during compile time 75 | * Fix issue #71 - client-credential cert-bad-cert failing 76 | * Fix issue #78 - Express dependency in package.json doesn't match syntax used in website-sample.js 77 | * Fix issue #80 - Unreachable ADFS server results in misleading error message 78 | 79 | Version 0.1.18 80 | -------------- 81 | Release Date: 5 Feb 2016 82 | * Add oid in IDTokenMap to expose for returned user info. 83 | 84 | Version 0.1.17 85 | -------------- 86 | Release Date: 8 Oct 2015 87 | * Add support for cross tenant refresh token 88 | 89 | Version 0.1.16 90 | -------------- 91 | Release Date: 3 Sep 2015 92 | * Add support for device profile 93 | 94 | Version 0.1.15 95 | -------------- 96 | Release Date: 4 Aug 2015 97 | * Fix issue #68 - add support for wstrust 2005 endpoint. 98 | * Fix issue #62 - escape xml chars in password before creating a xml request for wstrust. 99 | 100 | Version 0.1.14 101 | -------------- 102 | Release Date: 5 May 2015 103 | * Add timestamp to the log entries. 104 | 105 | Version 0.1.13 106 | -------------- 107 | Release Date: 1 May 2015 108 | * Scrub security sensitive data in WS-Trust exchange from log messages. 109 | * Update the version of the jws dependency to the latest release. 110 | 111 | Version 0.1.12 112 | -------------- 113 | Release Date: 19 February 2015 114 | * Add login.microsoftonline.com to the static list of known authorities. 115 | * Bug fixes. (#36) 116 | 117 | Version 0.1.11 118 | -------------- 119 | Release Date: 16 December 2014 120 | * Added support for certificate authentication for confidential clients. 121 | 122 | Version 0.1.10 123 | -------------- 124 | Release Date: 24 November 2014 125 | * 0.1.9 published version was corrupt. 126 | 127 | Version 0.1.9 128 | -------------- 129 | Release Date: 24 November 2014 130 | * Fixed issue #22 - ADAL needs a method that allows confidential clients 131 | to acquire a new token via a refresh token.The `acquireTokenWithRefreshToken` function was updated to take an additional `clientSecret` argument. Passing this new argument is optional and the change should not breakapps using this function before the additional argument was added. 132 | 133 | Version 0.1.8 134 | -------------- 135 | Release Date: 29 October 2014 136 | * Update version of xpath.js dependency to 1.0.5 137 | * Fix a bug in the regular expression used to parse a 401 challenge. 138 | 139 | Version 0.1.7 140 | -------------- 141 | Release Date: 2 September 2014 142 | * Add caching support to AuthenticationContext.acquireTokenWithClientCredentials 143 | -------------------------------------------------------------------------------- /test/self-signed-jwt.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | /* Directive tells jshint that suite and test are globals defined by mocha */ 24 | /* global suite */ 25 | /* global test */ 26 | 27 | const util = require('./util/util'); 28 | const cp = util.commonParameters; 29 | const testRequire = util.testRequire; 30 | 31 | const SelfSignedJwt = testRequire('self-signed-jwt'); 32 | 33 | import * as assert from "assert"; 34 | import * as sinon from "sinon"; 35 | 36 | const testNowDate = new Date(1418433646179); 37 | const testJwtId = '09841beb-a2c2-4777-a347-34ef055238a8'; 38 | const expectedJwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IndWM3FobGF0MzJlLWdESFlYcjNjV3RiRU51RSJ9.eyJhdWQiOiJodHRwczovL2xvZ2luLndpbmRvd3MubmV0L25hdHVyYWxjYXVzZXMuY29tL29hdXRoMi90b2tlbiIsImlzcyI6ImQ2ODM1NzEzLWI3NDUtNDhkMS1iYjYyLTdhODI0ODQ3N2QzNSIsInN1YiI6ImQ2ODM1NzEzLWI3NDUtNDhkMS1iYjYyLTdhODI0ODQ3N2QzNSIsIm5iZiI6MTQxODQzMzY0NiwiZXhwIjoxNDE4NDM0MjQ2LCJqdGkiOiIwOTg0MWJlYi1hMmMyLTQ3NzctYTM0Ny0zNGVmMDU1MjM4YTgifQ.AS3jyf9nUqBPeEFKccYA2NfSOSjDoWGW_QTj7Jqjbwpmp8jnQRkJ1Q9QrWLBIspesUVtctiKZQAl_BMochF_4yopY_JbYkPKEVvpbTojtwjKgTpVF175NUjXibUNCijx1BXRxEHJUbVJqzVSWBFtRCbXVBPg_ODqC0JJWutynnwMDec93gGOdWGi8AfRwj855zP41aDZGhQVFiOn3apzN4yfhOGoEeTbG4_6921Tkducz2jWpfVTxIS4yIOKCa97J6XInIlP1iW8XAsnGnTevanj8ubfCtYNRcCOrzq_qZstD6tSDqhQjJlTj5B0zlVvMjTT6oDTAOjzL4TuruENEg'; 39 | const testAuthority = {tokenEndpoint:'https://login.windows.net/naturalcauses.com/oauth2/token'}; 40 | const testClientId = 'd6835713-b745-48d1-bb62-7a8248477d35'; 41 | const testCert = util.getSelfSignedCert(); 42 | 43 | suite('self-signed-jwt', function() { 44 | function createJwt(cert: any, thumbprint: any) { 45 | var ssjwt = new SelfSignedJwt(cp.callContext, testAuthority, testClientId); 46 | sinon.stub(ssjwt, '_getDateNow').returns(testNowDate); 47 | sinon.stub(ssjwt, '_getNewJwtId').returns(testJwtId); 48 | var jwt = ssjwt.create(cert, thumbprint); 49 | return jwt; 50 | } 51 | 52 | function createJwtAndMatchExpected(cert: any, thumbprint: any) { 53 | var jwt = createJwt(cert, thumbprint); 54 | assert(jwt, 'No JWT generated'); 55 | assert(jwt === expectedJwt, 'Generated JWT does not match expected: ' + jwt); 56 | } 57 | 58 | test('create-jwt-hash-colons', function(done) { 59 | createJwtAndMatchExpected(testCert, cp.certHash); 60 | done(); 61 | }); 62 | 63 | test('create-jwt-hash-spaces', function(done) { 64 | var thumbprint = cp.certHash.replace(/:/g, ' '); 65 | createJwtAndMatchExpected(testCert, thumbprint); 66 | done(); 67 | }); 68 | 69 | test('create-jwt-hash-straight-hex', function(done) { 70 | var thumbprint = cp.certHash.replace(/:/g, ''); 71 | createJwtAndMatchExpected(testCert, thumbprint); 72 | done(); 73 | }); 74 | 75 | test('create-jwt-invalid-cert', function(done) { 76 | var expectedErr; 77 | try { 78 | createJwt('test', cp.certHash); 79 | } catch (err) { 80 | expectedErr = err; 81 | } 82 | assert(expectedErr, 'Did not receive expected error'); 83 | done(); 84 | }); 85 | 86 | test('create-jwt-invalid-thumbprint-1', function(done) { 87 | var expectedErr; 88 | try { 89 | createJwt(testCert, 'zzzz'); 90 | } catch (err) { 91 | expectedErr = err; 92 | } 93 | assert(expectedErr, 'Did not receive expected error'); 94 | done(); 95 | }); 96 | 97 | test('create-jwt-invalid-thumbprint-wrong-size', function(done) { 98 | var expectedErr; 99 | var thumbprint = 'C1:5D:EA:86:56:AD:DF:67:BE:80:31:D8:5E:BD:DC:5A:D6:C4:36:E7:AA'; 100 | try { 101 | createJwt(testCert, thumbprint); 102 | } catch (err) { 103 | expectedErr = err; 104 | } 105 | assert(expectedErr, 'Did not receive expected error'); 106 | done(); 107 | }); 108 | 109 | test('create-jwt-invalid-thumbprint-invalid-char', function(done) { 110 | var expectedErr; 111 | var thumbprint = 'C1:5D:EA:86:56:AD:DF:67:BE:80:31:D8:5E:BD:DC:5A:D6:C4:36:Ez'; 112 | try { 113 | createJwt(testCert, thumbprint); 114 | } catch (err) { 115 | expectedErr = err; 116 | } 117 | assert(expectedErr, 'Did not receive expected error'); 118 | done(); 119 | }); 120 | }); -------------------------------------------------------------------------------- /test/wstrust/common.base64.encoded.assertion.txt: -------------------------------------------------------------------------------- 1 | PHNhbWw6QXNzZXJ0aW9uIE1ham9yVmVyc2lvbj0iMSIgTWlub3JWZXJzaW9uPSIxIiBBc3NlcnRpb25JRD0iX2JmMTM3ZjkwLTdkZDctNDY2OC04YTM5LThiZjU1ZWI1MjAxNyIgSXNzdWVyPSJodHRwOi8vZnMubmF0dXJhbGNhdXNlcy5jb20vYWRmcy9zZXJ2aWNlcy90cnVzdCIgSXNzdWVJbnN0YW50PSIyMDE0LTAxLTI3VDA4OjE1OjQ1LjAwM1oiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMDphc3NlcnRpb24iPjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAxLTI3VDA4OjE1OjQ1LjAwM1oiIE5vdE9uT3JBZnRlcj0iMjAxNC0wMS0yN1QwOToxNTo0NS4wMDNaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uQ29uZGl0aW9uPjxzYW1sOkF1ZGllbmNlPnVybjpmZWRlcmF0aW9uOk1pY3Jvc29mdE9ubGluZTwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbkNvbmRpdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSWRlbnRpZmllciBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIj40M1lSelRyaCtVZThlVGVjbnFMQXhRPT08L3NhbWw6TmFtZUlkZW50aWZpZXI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48c2FtbDpDb25maXJtYXRpb25NZXRob2Q+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4wOmNtOmJlYXJlcjwvc2FtbDpDb25maXJtYXRpb25NZXRob2Q+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6QXR0cmlidXRlIEF0dHJpYnV0ZU5hbWU9IlVQTiIgQXR0cmlidXRlTmFtZXNwYWNlPSJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy9jbGFpbXMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlPmZyaXp6b0BuYXR1cmFsY2F1c2VzLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBBdHRyaWJ1dGVOYW1lPSJJbW11dGFibGVJRCIgQXR0cmlidXRlTmFtZXNwYWNlPSJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL0xpdmVJRC9GZWRlcmF0aW9uLzIwMDgvMDUiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlPjQzWVJ6VHJoK1VlOGVUZWNucUxBeFE9PTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXV0aGVudGljYXRpb25TdGF0ZW1lbnQgQXV0aGVudGljYXRpb25NZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMDphbTpwYXNzd29yZCIgQXV0aGVudGljYXRpb25JbnN0YW50PSIyMDE0LTAxLTI3VDA4OjE1OjQ0Ljk4N1oiPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlkZW50aWZpZXIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDp1bnNwZWNpZmllZCI+NDNZUnpUcmgrVWU4ZVRlY25xTEF4UT09PC9zYW1sOk5hbWVJZGVudGlmaWVyPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24+PHNhbWw6Q29uZmlybWF0aW9uTWV0aG9kPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMDpjbTpiZWFyZXI8L3NhbWw6Q29uZmlybWF0aW9uTWV0aG9kPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0Pjwvc2FtbDpBdXRoZW50aWNhdGlvblN0YXRlbWVudD48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz48ZHM6UmVmZXJlbmNlIFVSST0iI19iZjEzN2Y5MC03ZGQ3LTQ2NjgtOGEzOS04YmY1NWViNTIwMTciPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkI1ZUVJa1RoZ1M1NDQ1WkJaYVdXYmlxM1Vtdz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Yk0vQUVMOElzcWs3a0d4RFB2Y2lDN1JsbXI5ZmZSV0doa25wUmM0TFVDMEJqUWlZb28xMU5ZN3hoMFM1QSt4S0lvWE1nZWFhUDZxamIwbjI3VE5UM2craE9jQityOXg2SDVoQU5zNHJ0bC91T0VNMHBLdmNnQlkwYmh6NEhEUHFhaVJwQVZJdGdpU0dudERJZWc0MmNPaFNZSjlPbjZvR1FjVkE1aHkyR210eHhrN2Q3YzRTcTJueW0zdDNEM0RMTU9md3ZsdmlCWnNkQmdsVWc0aVpyUHU4cWdUUnJVbmZHaTVNMVZzVHVTVHdLVng3TkJOTGZqQnhBaXBtck0wUHlPSWs0M0NIQzNMSmpUQ3phajd2UlNxQmJYRjNMSW0yRGMwSFVMYlViZ3dPS3JvNzhyN2JkQ0Yzc0xmYlUyd2dUWnl2SWtYN0I3K29vdnN3RjZHb3l3PT08L2RzOlNpZ25hdHVyZVZhbHVlPjxLZXlJbmZvIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj48WDUwOURhdGE+PFg1MDlDZXJ0aWZpY2F0ZT5NSUlDNURDQ0FjeWdBd0lCQWdJUUdWTHpkbnZrQWFaSFp1ZlVpN3p2cVRBTkJna3Foa2lHOXcwQkFRc0ZBREF1TVN3d0tnWURWUVFERXlOQlJFWlRJRk5wWjI1cGJtY2dMU0JtY3k1dVlYUjFjbUZzWTJGMWMyVnpMbU52YlRBZUZ3MHhNekV4TVRBd01qRXhORGxhRncweE5ERXhNVEF3TWpFeE5EbGFNQzR4TERBcUJnTlZCQU1USTBGRVJsTWdVMmxuYm1sdVp5QXRJR1p6TG01aGRIVnlZV3hqWVhWelpYTXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF2c0psY2ttWjRqWkVKREJISG1kSDBDbGdPNENwNWJ2Z0FlTmVKTFFrSGxybkhEME81L2JreU1vNGFOZy9Gb0pKY0V0MFh6emQ4MTZsbkRZek0yVVRhV0Foa0hOYmw0c1ZXNGR0TFhlS0hlR3l5NDNVYlJZWWhJcERJSzhOaWN5UmRoeE5ualhiWkVNY3VROG5YcmJrajNETW5sQkVNLzVocFM3MzMreVZZclVrN0JjbXhhYjFsRFJVT0xiTDVLaDM1RzJKa1g4aUN4elVySFZqMTVEbmVHVlFHeUZPbWYyRHBDOUNOZXAxMjNYWWRYT2Z0WHQ0TmgxKy9lZDExemplWlhlUThobjM2LzNOSituNG1Ja0JlREdYbWhCZGg4TUFaV0NnVXgzRytTZGlmQzNiVlUzQnJWV2VQb2NUaUg2aGQxbGpMMmNWaDdoaEVJT3RHSEQ0S3dJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUNESXRobml2MUhrL05qSGtsQlhvVElYUFhXZVNhNThaWmZ0b3IwbzBxbHFaWDFoQWx1WHhKZUxLV3R1aUpLa1pvL2VUam9QaWRPM0wvWDBwc1pSK2NtTXNiRUE5dHRBTzhBa3FldVl4amx3MFlrZnFyWDZ5Uk5sa0dXcjRTVTA3WmdmSmhvVHNDUDlvT0JHL3gyeERrQUdmQUVkeUJ5RXZHa2F0MTZIZjJFTEEzZm9DcVVXOE1HSk0xNklEbXU2SzlLckdBT1IzZGEwUHdRNC9zRVRIa2gxQWc1amtZNjlsSjdyM01nemRNTVpEYzh0UFFhelZaYmUweGMxdThXRzUyYzJVR29heTl6TnFNUUpPR25VWk5COWZWSGF6QTJwdk1oQmJ5QlNxbzFqUzMxQlhMbG9kN3Y1TkVNOEY1QUdpa1V3WUhWK0VaL08ydDQ5MWdjRmpjOTwvWDUwOUNlcnRpZmljYXRlPjwvWDUwOURhdGE+PC9LZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjwvc2FtbDpBc3NlcnRpb24+ -------------------------------------------------------------------------------- /test/mex.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | /* Directive tells jshint that suite and test are globals defined by mocha */ 23 | /* global suite */ 24 | /* global test */ 25 | 26 | import * as assert from "assert"; 27 | import * as nock from "nock"; 28 | import * as fs from "fs"; 29 | 30 | const util = require('./util/util'); 31 | const cp = util.commonParameters; 32 | const testRequire = util.testRequire; 33 | 34 | const Mex = testRequire('mex'); 35 | 36 | /** 37 | * Tests the Mex class which does Mex retrieval and parsing. 38 | */ 39 | suite('MEX', function() { 40 | this.slow(200); // Tell mocha not to consider any of these tests 41 | // slow until after 200ms. XML parsing takes time! :( 42 | 43 | function setupExpectedMexResponse(filename: string) { 44 | var mexDoc = fs.readFileSync(__dirname + '/mex/' + filename, 'utf8'); 45 | var mexRequest = nock(cp.adfsUrlNoPath).get(cp.adfsMexPath).reply(200, mexDoc); 46 | 47 | util.matchStandardRequestHeaders(mexRequest); 48 | 49 | return mexRequest; 50 | } 51 | 52 | function happyPathTest(testFile: any, expectedUrl :string, done: Function) { 53 | var mexRequest = setupExpectedMexResponse(testFile); 54 | 55 | var mex = new Mex(cp.callContext, cp.adfsMex); 56 | mex.discover(function(err: Error) { 57 | if (!err) { 58 | assert(mex.usernamePasswordPolicy.url === expectedUrl, 59 | 'returned url did not match: ' + expectedUrl + ': ' + mex.usernamePasswordPolicy.url); 60 | mexRequest.done(); 61 | } 62 | done(); 63 | }); 64 | } 65 | 66 | test('happy-path-1', function(done) { 67 | happyPathTest('microsoft.mex.xml', 'https://corp.sts.microsoft.com/adfs/services/trust/13/usernamemixed', done); 68 | }); 69 | 70 | test('happy-path-2', function(done) { 71 | happyPathTest('arupela.mex.xml', 'https://fs.arupela.com/adfs/services/trust/13/usernamemixed', done); 72 | }); 73 | 74 | test('happy-path-3', function(done) { 75 | happyPathTest('archan.us.mex.xml', 'https://arvmserver2012.archan.us/adfs/services/trust/13/usernamemixed', done); 76 | }); 77 | 78 | test('happy-path-wstrust2005', function(done) { 79 | happyPathTest('usystech.mex.xml', 'https://sts.usystech.net/adfs/services/trust/2005/usernamemixed', done); 80 | }); 81 | 82 | function badMexDocTest(testFile: any, done: Function) { 83 | var mexRequest = setupExpectedMexResponse(testFile); 84 | 85 | var mex = new Mex(cp.callContext, cp.adfsMex); 86 | mex.discover(function(err: any) { 87 | mexRequest.done(); 88 | assert(err, 'badMexDocTest expected parsing error'); 89 | done(); 90 | }); 91 | } 92 | 93 | test('malformed-xml-1', function(done) { 94 | badMexDocTest('syntax.related.mex.xml', done); 95 | }); 96 | 97 | test('malformed-xml-2', function(done) { 98 | badMexDocTest('syntax.notrelated.mex.xml', done); 99 | }); 100 | 101 | test('logically-invalid-no-ssl', function(done) { 102 | badMexDocTest('address.insecure.xml', done); 103 | }); 104 | 105 | test('logically-invalid-no-address', function(done) { 106 | badMexDocTest('noaddress.xml', done); 107 | }); 108 | 109 | test('logically-invalid-no-binding-port', function(done) { 110 | badMexDocTest('nobinding.port.xml', done); 111 | }); 112 | 113 | test('logically-invalid-no-binding-port', function(done) { 114 | badMexDocTest('noname.binding.xml', done); 115 | }); 116 | 117 | test('logically-invalid-no-soap-action', function(done) { 118 | badMexDocTest('nosoapaction.xml', done); 119 | }); 120 | 121 | test('logically-invalid-no-soap-transport', function(done) { 122 | badMexDocTest('nosoaptransport.xml', done); 123 | }); 124 | 125 | test('logically-invalid-no-uri-ref', function(done) { 126 | badMexDocTest('nouri.ref.xml', done); 127 | }); 128 | 129 | test('failed-request', function(done) { 130 | var mexRequest = util.setupExpectedFailedMexCommon(); 131 | 132 | var mex = new Mex(cp.callContext, cp.adfsMex); 133 | mex.discover(function(err: any) { 134 | assert(err, 'Did not receive error as expected'); 135 | mexRequest.done(); 136 | done(); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /test/acquire-user-code.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 22 | 'use strict'; 23 | /* Directive tells jshint that suite and test are globals defined by mocha */ 24 | /* global suite */ 25 | /* global test */ 26 | 27 | import * as assert from "assert"; 28 | import * as nock from "nock"; 29 | import * as querystring from "querystring"; 30 | 31 | import * as adal from "../lib/adal"; 32 | const util = require('./util/util'); 33 | const cp = util.commonParameters; 34 | 35 | const AuthenticationContext = adal.AuthenticationContext; 36 | 37 | suite('acquire-user-code', function () { 38 | function setupExpectedDeviceCodeRequestResponse(withLanguage: boolean, httpCode: number, returnDoc: object, authorityEndpoint?: string) { 39 | var authEndpoint = util.getNockAuthorityHost(authorityEndpoint); 40 | 41 | var queryParameters: any = {}; 42 | queryParameters['client_id'] = cp.clientId; 43 | queryParameters['resource'] = cp.resource; 44 | if (withLanguage) { 45 | queryParameters['mkt'] = cp.language; 46 | } 47 | 48 | var query = querystring.stringify(queryParameters); 49 | 50 | var deviceCodeRequest = nock(authEndpoint) 51 | .filteringRequestBody(function (body: any) { 52 | return util.filterQueryString(query, body); 53 | }) 54 | .post(cp.deviceCodeUrlPath, query) 55 | .reply(httpCode, returnDoc); 56 | 57 | util.matchStandardRequestHeaders(deviceCodeRequest); 58 | 59 | return deviceCodeRequest; 60 | } 61 | 62 | test('happy-path', function (done) { 63 | var response = util.createDeviceCodeResponse(); 64 | var deviceCodeRequest = setupExpectedDeviceCodeRequestResponse(true, 200, response.wireResponse); 65 | 66 | var context = new AuthenticationContext(cp.authUrl); 67 | context.acquireUserCode(cp.resource, cp.clientId, cp.language, function (err, deviceCodeResponse) { 68 | assert(!err, 'Receive unexpected error. '); 69 | assert(util.isMathDeviceCodeResponse(response.decodedResponse, deviceCodeResponse), 'The response did not match what was expected'); 70 | deviceCodeRequest.done(); 71 | done(); 72 | }); 73 | }); 74 | 75 | test('happy-path-without-language', function (done) { 76 | var response = util.createDeviceCodeResponse(); 77 | var deviceCodeRequest = setupExpectedDeviceCodeRequestResponse(false, 200, response.wireResponse); 78 | 79 | var context = new AuthenticationContext(cp.authUrl); 80 | context.acquireUserCode(cp.resource, cp.clientId, null as any, function (err, deviceCodeResponse) { 81 | assert(!err, 'Receive unexpected error. '); 82 | assert(util.isMathDeviceCodeResponse(response.decodedResponse, deviceCodeResponse), 'The response did not match what was expected'); 83 | deviceCodeRequest.done(); 84 | done(); 85 | }); 86 | }); 87 | 88 | test('failed-http-request', function (done) { 89 | this.timeout(6000); 90 | this.slow(4000); 91 | 92 | nock.enableNetConnect(); 93 | var context = new AuthenticationContext('https://0.0.0.0:11/my.test.tenant.com'); 94 | context.acquireUserCode(cp.resource, cp.clientId, cp.language, function (err) { 95 | assert(err, 'Did not recieve expected error on failed http request.'); 96 | nock.disableNetConnect(); 97 | done(); 98 | }); 99 | }); 100 | 101 | test('bad-argument', function (done) { 102 | var context = new AuthenticationContext(cp.authUrl); 103 | 104 | context.acquireUserCode(null as any, cp.clientId, cp.language, function (err) { 105 | assert(err, 'Did not receive expected argument error'); 106 | assert(err.message === 'The resource parameter is required.') 107 | }); 108 | 109 | context.acquireUserCode(cp.resource, null as any, cp.language, function (err) { 110 | assert(err, 'Did not receive expected argument error'); 111 | assert(err.message === 'The clientId parameter is required.') 112 | }); 113 | 114 | try { 115 | context.acquireUserCode(cp.resource, cp.clientId, cp.language, null as any); 116 | } catch (e) { 117 | assert(e, 'Did not receive expected error. '); 118 | assert(e.message === 'acquireToken requires a function callback parameter.', 'Unexpected error message returned.'); 119 | } 120 | 121 | done(); 122 | }); 123 | 124 | }); -------------------------------------------------------------------------------- /sample/website-sample.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var express = require('express'); 24 | var logger = require('connect-logger'); 25 | var cookieParser = require('cookie-parser'); 26 | var session = require('cookie-session'); 27 | var fs = require('fs'); 28 | var crypto = require('crypto'); 29 | 30 | var AuthenticationContext = require('adal-node').AuthenticationContext; 31 | 32 | var app = express(); 33 | app.use(logger()); 34 | app.use(cookieParser('a deep secret')); 35 | app.use(session({secret: '1234567890QWERTY'})); 36 | 37 | app.get('/', function(req, res) { 38 | res.redirect('login'); 39 | }); 40 | 41 | /* 42 | * You can override the default account information by providing a JSON file 43 | * with the same parameters as the sampleParameters variable below. Either 44 | * through a command line argument, 'node sample.js parameters.json', or 45 | * specifying in an environment variable. 46 | * { 47 | * "tenant" : "rrandallaad1.onmicrosoft.com", 48 | * "authorityHostUrl" : "https://login.windows.net", 49 | * "clientId" : "624ac9bd-4c1c-4686-aec8-e56a8991cfb3", 50 | * "clientSecret" : "verySecret=" 51 | * } 52 | */ 53 | var parametersFile = process.argv[2] || process.env['ADAL_SAMPLE_PARAMETERS_FILE']; 54 | 55 | var sampleParameters; 56 | if (parametersFile) { 57 | var jsonFile = fs.readFileSync(parametersFile); 58 | if (jsonFile) { 59 | sampleParameters = JSON.parse(jsonFile); 60 | } else { 61 | console.log('File not found, falling back to defaults: ' + parametersFile); 62 | } 63 | } 64 | 65 | if (!parametersFile) { 66 | sampleParameters = { 67 | tenant : 'rrandallaad1.onmicrosoft.com', 68 | authorityHostUrl : 'https://login.windows.net', 69 | clientId : '624ac9bd-4c1c-4686-aec8-b56a8991cfb3', 70 | username : 'frizzo@naturalcauses.com', 71 | password : '' 72 | }; 73 | } 74 | 75 | var authorityUrl = sampleParameters.authorityHostUrl + '/' + sampleParameters.tenant; 76 | var redirectUri = 'http://localhost:3000/getAToken'; 77 | var resource = '00000002-0000-0000-c000-000000000000'; 78 | 79 | var templateAuthzUrl = 'https://login.windows.net/' + sampleParameters.tenant + '/oauth2/authorize?response_type=code&client_id=&redirect_uri=&state=&resource='; 80 | 81 | 82 | app.get('/', function(req, res) { 83 | res.redirect('/login'); 84 | }); 85 | 86 | app.get('/login', function(req, res) { 87 | console.log(req.cookies); 88 | 89 | res.cookie('acookie', 'this is a cookie'); 90 | 91 | res.send('\ 92 | \ 93 | test\ 94 | \ 95 | \ 96 | Login\ 97 | \ 98 | '); 99 | }); 100 | 101 | function createAuthorizationUrl(state) { 102 | var authorizationUrl = templateAuthzUrl.replace('', sampleParameters.clientId); 103 | authorizationUrl = authorizationUrl.replace('',redirectUri); 104 | authorizationUrl = authorizationUrl.replace('', state); 105 | authorizationUrl = authorizationUrl.replace('', resource); 106 | return authorizationUrl; 107 | } 108 | 109 | // Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD. 110 | // There they will authenticate and give their consent to allow this app access to 111 | // some resource they own. 112 | app.get('/auth', function(req, res) { 113 | crypto.randomBytes(48, function(ex, buf) { 114 | var token = buf.toString('base64').replace(/\//g,'_').replace(/\+/g,'-'); 115 | 116 | res.cookie('authstate', token); 117 | var authorizationUrl = createAuthorizationUrl(token); 118 | 119 | res.redirect(authorizationUrl); 120 | }); 121 | }); 122 | 123 | // After consent is granted AAD redirects here. The ADAL library is invoked via the 124 | // AuthenticationContext and retrieves an access token that can be used to access the 125 | // user owned resource. 126 | app.get('/getAToken', function(req, res) { 127 | if (req.cookies.authstate !== req.query.state) { 128 | res.send('error: state does not match'); 129 | } 130 | var authenticationContext = new AuthenticationContext(authorityUrl); 131 | authenticationContext.acquireTokenWithAuthorizationCode(req.query.code, redirectUri, resource, sampleParameters.clientId, sampleParameters.clientSecret, function(err, response) { 132 | var message = ''; 133 | if (err) { 134 | message = 'error: ' + err.message + '\n'; 135 | } 136 | message += 'response: ' + JSON.stringify(response); 137 | 138 | if (err) { 139 | res.send(message); 140 | return; 141 | } 142 | 143 | // Later, if the access token is expired it can be refreshed. 144 | authenticationContext.acquireTokenWithRefreshToken(response.refreshToken, sampleParameters.clientId, sampleParameters.clientSecret, resource, function(refreshErr, refreshResponse) { 145 | if (refreshErr) { 146 | message += 'refreshError: ' + refreshErr.message + '\n'; 147 | } 148 | message += 'refreshResponse: ' + JSON.stringify(refreshResponse); 149 | 150 | res.send(message); 151 | }); 152 | }); 153 | }); 154 | 155 | app.listen(3000); 156 | console.log('listening on 3000'); 157 | -------------------------------------------------------------------------------- /test/refresh-token.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | /* Directive tells jshint that suite and test are globals defined by mocha */ 23 | /* global suite */ 24 | /* global test */ 25 | 26 | 27 | // Run 28 | // npm test 29 | // from root of the repo 30 | import * as assert from "assert"; 31 | 32 | const util = require('./util/util'); 33 | const cp = util.commonParameters; 34 | 35 | import * as adal from "../lib/adal"; 36 | const AuthenticationContext = adal.AuthenticationContext; 37 | 38 | suite('refresh-token', function() { 39 | test('happy-path-no-resource', function(done) { 40 | var responseOptions = { refreshedRefresh : true }; 41 | var response = util.createResponse(responseOptions); 42 | var wireResponse = response.wireResponse; 43 | var tokenRequest = util.setupExpectedRefreshTokenRequestResponse(200, wireResponse, response.authority); 44 | 45 | var context = new AuthenticationContext(cp.authorityTenant); 46 | context.acquireTokenWithRefreshToken(cp.refreshToken, cp.clientId, null as any, null as any, function(err, tokenResponse) { 47 | if (!err) { 48 | tokenRequest.done(); 49 | assert(util.isMatchTokenResponse(response.decodedResponse, tokenResponse), 'The response did not match what was expected: ' + JSON.stringify(tokenResponse)); 50 | } 51 | done(err); 52 | }); 53 | }); 54 | 55 | test('happy-path-with-resource', function(done) { 56 | var responseOptions = { refreshedRefresh : true }; 57 | var response = util.createResponse(responseOptions); 58 | var wireResponse = response.wireResponse; 59 | var tokenRequest = util.setupExpectedRefreshTokenRequestResponse(200, wireResponse, response.authority, response.resource); 60 | 61 | var context = new AuthenticationContext(cp.authorityTenant); 62 | context.acquireTokenWithRefreshToken(cp.refreshToken, cp.clientId, null as any, cp.resource, function(err, tokenResponse) { 63 | if (!err) { 64 | tokenRequest.done(); 65 | assert(util.isMatchTokenResponse(response.decodedResponse, tokenResponse), 'The response did not match what was expected: ' + JSON.stringify(tokenResponse)) ; 66 | } 67 | done(err); 68 | }); 69 | }); 70 | 71 | test('happy-path-no-resource-client-secret', function(done) { 72 | var responseOptions = { refreshedRefresh : true }; 73 | var response = util.createResponse(responseOptions); 74 | var wireResponse = response.wireResponse; 75 | var tokenRequest = util.setupExpectedRefreshTokenRequestResponse(200, wireResponse, response.authority, null, cp.clientSecret); 76 | 77 | var context = new AuthenticationContext(cp.authorityTenant); 78 | context.acquireTokenWithRefreshToken(cp.refreshToken, cp.clientId, cp.clientSecret, null as any, function(err, tokenResponse) { 79 | if (!err) { 80 | tokenRequest.done(); 81 | assert(util.isMatchTokenResponse(response.decodedResponse, tokenResponse), 'The response did not match what was expected: ' + JSON.stringify(tokenResponse)); 82 | } 83 | done(err); 84 | }); 85 | }); 86 | 87 | test('happy-path-with-resource-client-secret', function(done) { 88 | var responseOptions = { refreshedRefresh : true }; 89 | var response = util.createResponse(responseOptions); 90 | var wireResponse = response.wireResponse; 91 | var tokenRequest = util.setupExpectedRefreshTokenRequestResponse(200, wireResponse, response.authority, response.resource, cp.clientSecret); 92 | 93 | var context = new AuthenticationContext(cp.authorityTenant); 94 | context.acquireTokenWithRefreshToken(cp.refreshToken, cp.clientId, cp.clientSecret, cp.resource, function(err, tokenResponse) { 95 | if (!err) { 96 | tokenRequest.done(); 97 | assert(util.isMatchTokenResponse(response.decodedResponse, tokenResponse), 'The response did not match what was expected: ' + JSON.stringify(tokenResponse)) ; 98 | } 99 | done(err); 100 | }); 101 | }); 102 | 103 | test('happy-path-no-resource-legacy', function(done) { 104 | var responseOptions = { refreshedRefresh : true }; 105 | var response = util.createResponse(responseOptions); 106 | var wireResponse = response.wireResponse; 107 | var tokenRequest = util.setupExpectedRefreshTokenRequestResponse(200, wireResponse, response.authority); 108 | 109 | var context = new AuthenticationContext(cp.authorityTenant); 110 | context.acquireTokenWithRefreshToken(cp.refreshToken, cp.clientId, null as any, function(err, tokenResponse) { 111 | if (!err) { 112 | tokenRequest.done(); 113 | assert(util.isMatchTokenResponse(response.decodedResponse, tokenResponse), 'The response did not match what was expected: ' + JSON.stringify(tokenResponse)); 114 | } 115 | done(err); 116 | }); 117 | }); 118 | 119 | test('happy-path-with-resource-legacy', function(done) { 120 | var responseOptions = { refreshedRefresh : true }; 121 | var response = util.createResponse(responseOptions); 122 | var wireResponse = response.wireResponse; 123 | var tokenRequest = util.setupExpectedRefreshTokenRequestResponse(200, wireResponse, response.authority, response.resource); 124 | 125 | var context = new AuthenticationContext(cp.authorityTenant); 126 | context.acquireTokenWithRefreshToken(cp.refreshToken, cp.clientId, cp.resource, function(err, tokenResponse) { 127 | if (!err) { 128 | tokenRequest.done(); 129 | assert(util.isMatchTokenResponse(response.decodedResponse, tokenResponse), 'The response did not match what was expected: ' + JSON.stringify(tokenResponse)) ; 130 | } 131 | done(err); 132 | }); 133 | }); 134 | 135 | }); -------------------------------------------------------------------------------- /test/wstrust/common.rstr.xml: -------------------------------------------------------------------------------- 1 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal2014-01-27T08:15:45.003Z2014-01-27T08:20:45.003Z2014-01-27T08:15:45.003Z2014-01-27T09:15:45.003Zurn:federation:MicrosoftOnlineurn:federation:MicrosoftOnline43YRzTrh+Ue8eTecnqLAxQ==urn:oasis:names:tc:SAML:1.0:cm:bearerfrizzo@naturalcauses.com43YRzTrh+Ue8eTecnqLAxQ==43YRzTrh+Ue8eTecnqLAxQ==urn:oasis:names:tc:SAML:1.0:cm:bearerB5eEIkThgS5445ZBZaWWbiq3Umw=bM/AEL8Isqk7kGxDPvciC7Rlmr9ffRWGhknpRc4LUC0BjQiYoo11NY7xh0S5A+xKIoXMgeaaP6qjb0n27TNT3g+hOcB+r9x6H5hANs4rtl/uOEM0pKvcgBY0bhz4HDPqaiRpAVItgiSGntDIeg42cOhSYJ9On6oGQcVA5hy2Gmtxxk7d7c4Sq2nym3t3D3DLMOfwvlviBZsdBglUg4iZrPu8qgTRrUnfGi5M1VsTuSTwKVx7NBNLfjBxAipmrM0PyOIk43CHC3LJjTCzaj7vRSqBbXF3LIm2Dc0HULbUbgwOKro78r7bdCF3sLfbU2wgTZyvIkX7B7+oovswF6Goyw==MIIC5DCCAcygAwIBAgIQGVLzdnvkAaZHZufUi7zvqTANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNBREZTIFNpZ25pbmcgLSBmcy5uYXR1cmFsY2F1c2VzLmNvbTAeFw0xMzExMTAwMjExNDlaFw0xNDExMTAwMjExNDlaMC4xLDAqBgNVBAMTI0FERlMgU2lnbmluZyAtIGZzLm5hdHVyYWxjYXVzZXMuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvsJlckmZ4jZEJDBHHmdH0ClgO4Cp5bvgAeNeJLQkHlrnHD0O5/bkyMo4aNg/FoJJcEt0Xzzd816lnDYzM2UTaWAhkHNbl4sVW4dtLXeKHeGyy43UbRYYhIpDIK8NicyRdhxNnjXbZEMcuQ8nXrbkj3DMnlBEM/5hpS733+yVYrUk7Bcmxab1lDRUOLbL5Kh35G2JkX8iCxzUrHVj15DneGVQGyFOmf2DpC9CNep123XYdXOftXt4Nh1+/ed11zjeZXeQ8hn36/3NJ+n4mIkBeDGXmhBdh8MAZWCgUx3G+SdifC3bVU3BrVWePocTiH6hd1ljL2cVh7hhEIOtGHD4KwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCDIthniv1Hk/NjHklBXoTIXPXWeSa58ZZftor0o0qlqZX1hAluXxJeLKWtuiJKkZo/eTjoPidO3L/X0psZR+cmMsbEA9ttAO8AkqeuYxjlw0YkfqrX6yRNlkGWr4SU07ZgfJhoTsCP9oOBG/x2xDkAGfAEdyByEvGkat16Hf2ELA3foCqUW8MGJM16IDmu6K9KrGAOR3da0PwQ4/sETHkh1Ag5jkY69lJ7r3MgzdMMZDc8tPQazVZbe0xc1u8WG52c2UGoay9zNqMQJOGnUZNB9fVHazA2pvMhBbyBSqo1jS31BXLlod7v5NEM8F5AGikUwYHV+EZ/O2t491gcFjc9_bf137f90-7dd7-4668-8a39-8bf55eb52017_bf137f90-7dd7-4668-8a39-8bf55eb52017urn:oasis:names:tc:SAML:1.0:assertionhttp://docs.oasis-open.org/ws-sx/ws-trust/200512/Issuehttp://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | # Microsoft Identity SDK Versioning and Servicing FAQ 2 | 3 | We have adopted the semantic versioning flow that is industry standard for OSS projects. It gives the maximum amount of control on what risk you take with what versions. If you know how semantic versioning works with node.js, java, and ruby none of this will be new. 4 | 5 | ##Semantic Versioning and API stability promises 6 | 7 | Microsoft Identity libraries are independent open source libraries that are used by partners both internal and external to Microsoft. As with the rest of Microsoft, we have moved to a rapid iteration model where bugs are fixed daily and new versions are produced as required. To communicate these frequent changes to external partners and customers, we use semantic versioning for all our public Microsoft Identity SDK libraries. This follows the practices of other open source libraries on the internet. This allows us to support our downstream partners which will lock on certain versions for stability purposes, as well as providing for the distribution over NuGet, CocoaPods, and Maven. 8 | 9 | The semantics are: MAJOR.MINOR.PATCH (example 1.1.5) 10 | 11 | We will update our code distributions to use the latest PATCH semantic version number in order to make sure our customers and partners get the latest bug fixes. Downstream partner needs to pull the latest PATCH version. Most partners should try lock on the latest MINOR version number in their builds and accept any updates in the PATCH number. 12 | 13 | Examples: 14 | Using Cocapods, the following in the podfile will take the latest ADALiOS build that is > 1.1 but not 1.2. 15 | ``` 16 | pod 'ADALiOS', '~> 1.1' 17 | ``` 18 | 19 | Using NuGet, this ensures all 1.1.0 to 1.1.x updates are included when building your code, but not 1.2. 20 | 21 | ``` 22 | 26 | ``` 27 | 28 | | Version | Description | Example | 29 | |:-------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------:| 30 | | x.x.x | PATCH version number. Incrementing these numbers is for bug fixes and updates but do not introduce new features. This is used for close partners who build on our platform release (ex. Azure AD Fabric, Office, etc.),In addition, Cocoapods, NuGet, and Maven use this number to deliver the latest release to customers.,This will update frequently (sometimes within the same day),There is no new features, and no regressions or API surface changes. Code will continue to work unless affected by a particular code fix. | ADAL for iOS 1.0.10,(this was a fix for the Storyboard display that was fixed for a specific Office team) | 31 | | x.x | MINOR version numbers. Incrementing these second numbers are for new feature additions that do not impact existing features or introduce regressions. They are purely additive, but may require testing to ensure nothing is impacted.,All x.x.x bug fixes will also roll up in to this number.,There is no regressions or API surface changes. Code will continue to work unless affected by a particular code fix or needs this new feature. | ADAL for iOS 1.1.0,(this added WPJ capability to ADAL, and rolled all the updates from 1.0.0 to 1.0.12) | 32 | | x | MAJOR version numbers. This should be considered a new, supported version of Microsoft Identity SDK and begins the Azure two year support cycle anew. Major new features are introduced and API changes can occur.,This should only be used after a large amount of testing and used only if those features are needed.,We will continue to service MAJOR version numbers with bug fixes up to the two year support cycle. | ADAL for iOS 1.0,(our first official release of ADAL) | 33 | 34 | 35 | 36 | ## Serviceability 37 | 38 | When we release a new MINOR version, the previous MINOR version is abandoned. 39 | 40 | When we release a new MAJOR version, we will continue to apply bug fixes to the existing features in the previous MAJOR version for up to the 2 year support cycle for Azure. 41 | Example: We release ADALiOS 2.0 in the future which supports unified Auth for AAD and MSA. Later, we then have a fix in Conditional Access for ADALiOS. Since that feature exists both in ADALiOS 1.1 and ADALiOS 2.0, we will fix both. It will roll up in a PATCH number for each. Customers that are still locked down on ADALiOS 1.1 will receive the benefit of this fix. 42 | 43 | ## Microsoft Identity SDKs and Azure Active Directory 44 | 45 | Microsoft Identity SDKs major versions will maintain backwards compatibility with Azure Active Directory web services through the support period. This means that the API surface area defined in a MAJOR version will continue to work for 2 years after release. 46 | 47 | We will respond to bugs quickly from our partners and customers submitted through GitHub and through our private alias (tellaad@microsoft.com) for security issues and update the PATCH version number. We will also submit a change summary for each PATCH number. 48 | Occasionally, there will be security bugs or breaking bugs from our partners that will require an immediate fix and a publish of an update to all partners and customers. When this occurs, we will do an emergency roll up to a PATCH version number and update all our distribution methods to the latest. 49 | -------------------------------------------------------------------------------- /lib/self-signed-jwt.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var jwtConstants = require('./constants').Jwt; 24 | var Logger = require('./log').Logger; 25 | var util = require('./util'); 26 | 27 | require('date-utils'); 28 | var jws = require('jws'); 29 | var uuid = require('uuid'); 30 | 31 | /** 32 | * JavaScript dates are in milliseconds, but JWT dates are in seconds. 33 | * This function does the conversion. 34 | * @param {Date} date 35 | * @return {string} 36 | */ 37 | function dateGetTimeInSeconds(date) { 38 | return Math.floor(date.getTime()/1000); 39 | } 40 | 41 | /** 42 | * Constructs a new SelfSignedJwt object. 43 | * @param {object} callContext Context specific to this token request. 44 | * @param {Authority} authority The authority to be used as the JWT audience. 45 | * @param {string} clientId The client id of the calling app. 46 | */ 47 | function SelfSignedJwt(callContext, authority, clientId) { 48 | this._log = new Logger('SelfSignedJwt', callContext._logContext); 49 | this._callContext = callContext; 50 | 51 | this._authority = authority; 52 | this._tokenEndpoint = authority.tokenEndpoint; 53 | this._clientId = clientId; 54 | } 55 | 56 | /** 57 | * This wraps date creation in order to make unit testing easier. 58 | * @return {Date} 59 | */ 60 | SelfSignedJwt.prototype._getDateNow = function() { 61 | return new Date(); 62 | }; 63 | 64 | SelfSignedJwt.prototype._getNewJwtId = function() { 65 | return uuid.v4(); 66 | }; 67 | 68 | /** 69 | * A regular certificate thumbprint is a hex encode string of the binary certificate 70 | * hash. For some reason teh x5t value in a JWT is a url save base64 encoded string 71 | * instead. This function does the conversion. 72 | * @param {string} thumbprint A hex encoded certificate thumbprint. 73 | * @return {string} A url safe base64 encoded certificate thumbprint. 74 | */ 75 | SelfSignedJwt.prototype._createx5tValue = function(thumbprint) { 76 | var hexString = thumbprint.replace(/:/g, '').replace(/ /g, ''); 77 | var base64 = (new Buffer(hexString, 'hex')).toString('base64'); 78 | return util.convertRegularToUrlSafeBase64EncodedString(base64); 79 | }; 80 | 81 | /** 82 | * Creates the JWT header. 83 | * @param {string} thumbprint A hex encoded certificate thumbprint. 84 | * @return {object} 85 | */ 86 | SelfSignedJwt.prototype._createHeader = function(thumbprint) { 87 | var x5t = this._createx5tValue(thumbprint); 88 | var header = { typ: 'JWT', alg: 'RS256', x5t : x5t }; 89 | 90 | this._log.verbose('Creating self signed JWT header'); 91 | this._log.verbose('Creating self signed JWT header. x5t: ' + x5t, true); 92 | 93 | return header; 94 | }; 95 | 96 | /** 97 | * Creates the JWT payload. 98 | * @return {object} 99 | */ 100 | SelfSignedJwt.prototype._createPayload = function() { 101 | var now = this._getDateNow(); 102 | var expires = (new Date(now.getTime())).addMinutes(jwtConstants.SELF_SIGNED_JWT_LIFETIME); 103 | 104 | this._log.verbose('Creating self signed JWT payload. Expires: ' + expires + ' NotBefore: ' + now); 105 | 106 | var jwtPayload = {}; 107 | jwtPayload[jwtConstants.AUDIENCE] = this._tokenEndpoint; 108 | jwtPayload[jwtConstants.ISSUER] = this._clientId; 109 | jwtPayload[jwtConstants.SUBJECT] = this._clientId; 110 | jwtPayload[jwtConstants.NOT_BEFORE] = dateGetTimeInSeconds(now); 111 | jwtPayload[jwtConstants.EXPIRES_ON] = dateGetTimeInSeconds(expires); 112 | jwtPayload[jwtConstants.JWT_ID] = this._getNewJwtId(); 113 | 114 | return jwtPayload; 115 | }; 116 | 117 | SelfSignedJwt.prototype._throwOnInvalidJwtSignature = function(jwt) { 118 | var jwtSegments = jwt.split('.'); 119 | 120 | if (3 > jwtSegments.length || !jwtSegments[2]) { 121 | throw this._log.createError('Failed to sign JWT. This is most likely due to an invalid certificate.'); 122 | } 123 | 124 | return; 125 | }; 126 | 127 | SelfSignedJwt.prototype._signJwt = function(header, payload, certificate) { 128 | var jwt; 129 | try { 130 | jwt = jws.sign({ header : header, payload : payload, secret : certificate }); 131 | } 132 | catch (err) { 133 | this._log.error(err, true); 134 | throw this._log.createError('Failed to sign JWT.This is most likely due to an invalid certificate.'); 135 | } 136 | 137 | this._throwOnInvalidJwtSignature(jwt); 138 | return jwt; 139 | }; 140 | 141 | SelfSignedJwt.prototype._reduceThumbprint = function(thumbprint) { 142 | var canonical = thumbprint.toLowerCase().replace(/ /g, '').replace(/:/g, ''); 143 | this._throwOnInvalidThumbprint(canonical); 144 | return canonical; 145 | }; 146 | 147 | var numCharIn128BitHexString = 128/8*2; 148 | var numCharIn160BitHexString = 160/8*2; 149 | var thumbprintSizes = {}; 150 | thumbprintSizes[numCharIn128BitHexString] = true; 151 | thumbprintSizes[numCharIn160BitHexString] = true; 152 | var thumbprintRegExp = /^[a-f\d]*$/; 153 | 154 | SelfSignedJwt.prototype._throwOnInvalidThumbprint = function(thumbprint) { 155 | if (!thumbprintSizes[thumbprint.length] || !thumbprintRegExp.test(thumbprint)) { 156 | throw this._log.createError('The thumbprint does not match a known format'); 157 | } 158 | }; 159 | 160 | /** 161 | * Creates a self signed JWT that can be used as a client_assertion. 162 | * @param {string} certificate A PEM encoded certificate private key. 163 | * @param {string} thumbprint A hex encoded thumbprint of the certificate. 164 | * @return {string} A self signed JWT token. 165 | */ 166 | SelfSignedJwt.prototype.create = function(certificate, thumbprint) { 167 | thumbprint = this._reduceThumbprint(thumbprint); 168 | var header = this._createHeader(thumbprint); 169 | 170 | var payload = this._createPayload(); 171 | 172 | var jwt = this._signJwt(header, payload, certificate); 173 | return jwt; 174 | }; 175 | 176 | module.exports = SelfSignedJwt; -------------------------------------------------------------------------------- /test/wstrust-response.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 22 | 'use strict'; 23 | /* Directive tells jshint that suite and test are globals defined by mocha */ 24 | /* global suite */ 25 | /* global test */ 26 | 27 | import * as assert from "assert"; 28 | import * as fs from "fs"; 29 | 30 | const util = require('./util/util'); 31 | const cp = util.commonParameters; 32 | const testRequire = util.testRequire; 33 | 34 | const WSTrustResponse = testRequire('wstrust-response'); 35 | const WSTrustVersion = testRequire('constants').WSTrustVersion; 36 | 37 | /** 38 | * Tests the WSTrustResponse class which parses a ws-trust RSTR. 39 | */ 40 | suite('WSTrustResponse', function() { 41 | test('parse-error-happy-path', function(done) { 42 | var errorResponse = '\ 43 | \ 44 | \ 45 | http://www.w3.org/2005/08/addressing/soap/fault\ 46 | \ 47 | \ 48 | 2013-07-30T00:32:21.989Z\ 49 | 2013-07-30T00:37:21.989Z\ 50 | \ 51 | \ 52 | \ 53 | \ 54 | \ 55 | \ 56 | s:Sender\ 57 | \ 58 | a:RequestFailed\ 59 | \ 60 | \ 61 | \ 62 | MSIS3127: The specified request failed.\ 63 | \ 64 | \ 65 | \ 66 | \ 67 | '; 68 | 69 | var wstrustResponse = new WSTrustResponse(cp.callContext, errorResponse); 70 | 71 | var thrownError; 72 | try { 73 | wstrustResponse.parse(); 74 | } catch (err) { 75 | thrownError = err; 76 | } 77 | 78 | assert(thrownError, 'Exppected an exception but none was thrown'); 79 | assert(wstrustResponse.errorCode === 'RequestFailed', 'wstrustResponse.ErrorCode does not match expected value: ' + wstrustResponse.errorCode); 80 | assert(-1 !== wstrustResponse.faultMessage.indexOf('MSIS3127: The specified request failed.'), 'wstrustResponse.FaultMessage does not match expected value: ' + wstrustResponse.faultMessage); 81 | 82 | done(); 83 | }); 84 | 85 | test('token-parsing-happy-path', function(done) { 86 | var tokenResponse = fs.readFileSync(__dirname + '/wstrust/RSTR.xml', 'utf8'); 87 | 88 | var wstrustResponse = new WSTrustResponse(cp.callContext, tokenResponse); 89 | 90 | wstrustResponse.parse(); 91 | assert(wstrustResponse.tokenType === 'urn:oasis:names:tc:SAML:1.0:assertion', 'TokenType did not match expected value: ' + wstrustResponse.tokenType); 92 | assert(-1 !== wstrustResponse.token.indexOf('1TIu064jGEmmf+hnI+F0Jg=='), 'Did not find expected ImmutableID value in Token property: ' + wstrustResponse.token); 93 | done(); 94 | }); 95 | 96 | test('rstr-undefined', function(done) { 97 | var wstrustResponse = new WSTrustResponse(cp.callContext, undefined); 98 | var thrownError; 99 | try { 100 | wstrustResponse.parse(); 101 | } catch (err) { 102 | thrownError = err; 103 | } 104 | 105 | assert(thrownError, 'Did not receive expected exception'); 106 | assert(-1 !== thrownError.message.indexOf('empty'), 'Did not receive expected error message: ' + thrownError); 107 | assert(!wstrustResponse.errorCode, 'Received ErrorCode when none was expected: ' + wstrustResponse.errorCode); 108 | assert(!wstrustResponse.faultMessage, 'Received FaultMessage when none was expected' + wstrustResponse.faultMessage); 109 | done(); 110 | }); 111 | 112 | test('rstr-empty-string', function(done) { 113 | var wstrustResponse = new WSTrustResponse(cp.callContext, ''); 114 | var thrownError; 115 | try { 116 | wstrustResponse.parse(); 117 | } catch (err) { 118 | thrownError = err; 119 | } 120 | 121 | assert(thrownError, 'Did not receive expected exception'); 122 | assert(-1 !== thrownError.message.indexOf('empty'), 'Did not receive expected error message: ' + thrownError.stack); 123 | assert(!wstrustResponse.errorCode, 'Received ErrorCode when none was expected: ' + wstrustResponse.errorCode); 124 | assert(!wstrustResponse.faultMessage, 'Received FaultMessage when none was expected' + wstrustResponse.faultMessage); 125 | done(); 126 | }); 127 | 128 | test('rstr-unparseable-xml', function(done) { 129 | var wstrustResponse = new WSTrustResponse(cp.callContext, '= 200 && statusCode < 300; 50 | } 51 | 52 | function addDefaultRequestHeaders (self, options) { 53 | if (!options.headers) { 54 | options.headers = {}; 55 | } 56 | var headers = options.headers; 57 | if (!headers['Accept-Charset']) { 58 | headers['Accept-Charset'] = 'utf-8'; 59 | } 60 | headers['client-request-id'] = self._callContext._logContext.correlationId; 61 | headers['return-client-request-id'] = 'true'; 62 | 63 | // ADAL Id headers 64 | headers[adalIdConstants.SKU] = adalIdConstants.NODE_SKU; 65 | headers[adalIdConstants.VERSION] = ADAL_VERSION; 66 | headers[adalIdConstants.OS] = os.platform(); 67 | headers[adalIdConstants.CPU] = os.arch(); 68 | } 69 | 70 | /** 71 | * Central place for housing default request options. This is a place holder 72 | * for when SSL validation is implemented an all requests are subject to that 73 | * policy. 74 | * @static 75 | * @memberOf Util 76 | * @param {object} options A set of options that will be merged with teh default options 77 | * These will override any default options. 78 | * @returns {object} Returns the merged options. 79 | */ 80 | function createRequestOptions(self, options) { 81 | var defaultOptions = {}; //{ strictSSL : true }; 82 | var mergedOptions = defaultOptions; 83 | if (options) { 84 | _.extend(mergedOptions, options); 85 | } 86 | if (self._callContext.options && self._callContext.options.http) { 87 | _.extend(mergedOptions, self._callContext.options.http); 88 | } 89 | 90 | addDefaultRequestHeaders(self, mergedOptions); 91 | return mergedOptions; 92 | } 93 | 94 | function logReturnCorrelationId(log, operationMessage, response) { 95 | if (response && response.headers && response.headers['client-request-id']) { 96 | log.info(operationMessage + 'Server returned this correlationId: ' + response.headers['client-request-id'], true); 97 | } 98 | } 99 | 100 | /** 101 | * Creates a function that can be used as the callback for http request operations. This is meant 102 | * to centralize error handling in one place. 103 | * @static 104 | * @memberOf Util 105 | * @param {string} operationMessage A message to be prepended to logged error strings. This should be something like 'Mex Request' 106 | * and summarize the purpose of the http request. 107 | * @param {object} log A Logger object being used by the calling component. 108 | * @param {Util.CreateRequestHandlerErrorCallback} errorCallback Called in the event of an error. 109 | * @param {Util.CreateRequestHandlerSuccessCallabck} successCallback Called on successfull completion of the request. 110 | */ 111 | function createRequestHandler(operationMessage, log, errorCallback, successCallback) { 112 | return function(err, response, body) { 113 | logReturnCorrelationId(log, operationMessage, response); 114 | if (err) { 115 | log.error(operationMessage + ' request failed with', err, true); 116 | errorCallback(err); 117 | return; 118 | } 119 | if (!isHttpSuccess(response.statusCode)) { 120 | var returnErrorString = operationMessage + ' request returned http error: ' + response.statusCode; 121 | var errorResponse; 122 | if (body) { 123 | returnErrorString += ' and server response: ' + body; 124 | try { 125 | errorResponse = JSON.parse(body); 126 | } catch (e) { 127 | // No problem if it doesn't parse. 128 | } 129 | } 130 | errorCallback(log.createError(returnErrorString, true), errorResponse); 131 | return; 132 | } 133 | 134 | successCallback(response, body); 135 | }; 136 | } 137 | 138 | /** 139 | * @callback CreateRequestHandlerErrorCallback 140 | * @memberOf Util 141 | * @param {Error} error An error object. 142 | */ 143 | 144 | /** 145 | * @callback CreateRequestHandlerSuccessCallabck 146 | * @memberOf Util 147 | * @param {object} response The response object returned from request. 148 | * @param {string} body The body of the http response. 149 | */ 150 | 151 | /** 152 | * Deep copies a url object. 153 | * @static 154 | * @memberOf Util 155 | * @param {URL} urlSource The source url object to copy. 156 | * @returns {URL} A deep copy of sourceUrl. 157 | */ 158 | function copyUrl(urlSource) { 159 | return url.parse(url.format(urlSource)); 160 | } 161 | 162 | function convertUrlSafeToRegularBase64EncodedString(str) { 163 | return str.replace(/-/g, '+').replace(/_/g, '/'); 164 | } 165 | 166 | function convertRegularToUrlSafeBase64EncodedString(str) { 167 | return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); 168 | } 169 | 170 | function base64DecodeStringUrlSafe(str) { 171 | var base64 = convertUrlSafeToRegularBase64EncodedString(str); 172 | return (new Buffer(base64, 'base64')).toString('utf8'); 173 | } 174 | 175 | function base64EncodeStringUrlSafe(str) { 176 | var base64 = (new Buffer(str, 'utf8').toString('base64')); 177 | var converted = convertRegularToUrlSafeBase64EncodedString(base64); 178 | return converted; 179 | } 180 | 181 | module.exports.adalInit = adalInit; 182 | module.exports.isHttpSuccess = isHttpSuccess; 183 | module.exports.logReturnCorrelationId = logReturnCorrelationId; 184 | module.exports.createRequestHandler = createRequestHandler; 185 | module.exports.createRequestOptions = createRequestOptions; 186 | module.exports.copyUrl = copyUrl; 187 | module.exports.base64DecodeStringUrlSafe = base64DecodeStringUrlSafe; 188 | module.exports.base64EncodeStringUrlSafe = base64EncodeStringUrlSafe; 189 | module.exports.convertRegularToUrlSafeBase64EncodedString = convertRegularToUrlSafeBase64EncodedString; -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var Constants = { 24 | OAuth2 : { 25 | Parameters : { 26 | GRANT_TYPE : 'grant_type', 27 | CLIENT_ASSERTION : 'client_assertion', 28 | CLIENT_ASSERTION_TYPE : 'client_assertion_type', 29 | CLIENT_ID : 'client_id', 30 | CLIENT_SECRET : 'client_secret', 31 | REDIRECT_URI : 'redirect_uri', 32 | RESOURCE : 'resource', 33 | CODE : 'code', 34 | SCOPE : 'scope', 35 | ASSERTION : 'assertion', 36 | AAD_API_VERSION : 'api-version', 37 | USERNAME : 'username', 38 | PASSWORD : 'password', 39 | REFRESH_TOKEN : 'refresh_token', 40 | LANGUAGE : 'mkt', 41 | DEVICE_CODE : 'device_code', 42 | }, 43 | 44 | GrantType : { 45 | AUTHORIZATION_CODE : 'authorization_code', 46 | REFRESH_TOKEN : 'refresh_token', 47 | CLIENT_CREDENTIALS : 'client_credentials', 48 | JWT_BEARER : 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 49 | PASSWORD : 'password', 50 | SAML1 : 'urn:ietf:params:oauth:grant-type:saml1_1-bearer', 51 | SAML2 : 'urn:ietf:params:oauth:grant-type:saml2-bearer', 52 | DEVICE_CODE: 'device_code' 53 | }, 54 | 55 | ResponseParameters : { 56 | CODE : 'code', 57 | TOKEN_TYPE : 'token_type', 58 | ACCESS_TOKEN : 'access_token', 59 | ID_TOKEN : 'id_token', 60 | REFRESH_TOKEN : 'refresh_token', 61 | CREATED_ON : 'created_on', 62 | EXPIRES_ON : 'expires_on', 63 | EXPIRES_IN : 'expires_in', 64 | RESOURCE : 'resource', 65 | ERROR : 'error', 66 | ERROR_DESCRIPTION : 'error_description' 67 | }, 68 | 69 | DeviceCodeResponseParameters : { 70 | USER_CODE : 'user_code', 71 | DEVICE_CODE : 'device_code', 72 | VERIFICATION_URL : 'verification_url', 73 | EXPIRES_IN : 'expires_in', 74 | INTERVAL: 'interval', 75 | MESSAGE: 'message', 76 | ERROR: 'error', 77 | ERROR_DESCRIPTION: 'error_description' 78 | }, 79 | 80 | Scope : { 81 | OPENID : 'openid' 82 | }, 83 | 84 | IdTokenMap : { 85 | 'tid' : 'tenantId', 86 | 'given_name' : 'givenName', 87 | 'family_name' : 'familyName', 88 | 'idp' : 'identityProvider', 89 | 'oid': 'oid' 90 | } 91 | }, 92 | 93 | TokenResponseFields : { 94 | TOKEN_TYPE : 'tokenType', 95 | ACCESS_TOKEN : 'accessToken', 96 | REFRESH_TOKEN : 'refreshToken', 97 | CREATED_ON : 'createdOn', 98 | EXPIRES_ON : 'expiresOn', 99 | EXPIRES_IN : 'expiresIn', 100 | RESOURCE : 'resource', 101 | USER_ID : 'userId', 102 | ERROR : 'error', 103 | ERROR_DESCRIPTION : 'errorDescription' 104 | }, 105 | 106 | UserCodeResponseFields : { 107 | USER_CODE : 'userCode', 108 | DEVICE_CODE: 'deviceCode', 109 | VERIFICATION_URL: 'verificationUrl', 110 | EXPIRES_IN: 'expiresIn', 111 | INTERVAL: 'interval', 112 | MESSAGE: 'message', 113 | ERROR: 'error', 114 | ERROR_DESCRIPTION: 'errorDescription' 115 | }, 116 | 117 | IdTokenFields : { 118 | USER_ID : 'userId', 119 | IS_USER_ID_DISPLAYABLE : 'isUserIdDisplayable', 120 | TENANT_ID : 'tenantId', 121 | GIVE_NAME : 'givenName', 122 | FAMILY_NAME : 'familyName', 123 | IDENTITY_PROVIDER : 'identityProvider' 124 | }, 125 | 126 | Misc : { 127 | MAX_DATE : 0xffffffff, 128 | CLOCK_BUFFER : 5 // In minutes. 129 | }, 130 | 131 | Jwt : { 132 | SELF_SIGNED_JWT_LIFETIME : 10, // 10 mins in mins 133 | AUDIENCE : 'aud', 134 | ISSUER : 'iss', 135 | SUBJECT : 'sub', 136 | NOT_BEFORE : 'nbf', 137 | EXPIRES_ON : 'exp', 138 | JWT_ID : 'jti' 139 | }, 140 | 141 | AADConstants : { 142 | WORLD_WIDE_AUTHORITY : 'login.windows.net', 143 | WELL_KNOWN_AUTHORITY_HOSTS : ['login.windows.net', 'login.microsoftonline.com', 'login.chinacloudapi.cn', 'login-us.microsoftonline.com', 'login.microsoftonline.de', 'login.microsoftonline.us'], 144 | INSTANCE_DISCOVERY_ENDPOINT_TEMPLATE : 'https://{authorize_host}/common/discovery/instance?authorization_endpoint={authorize_endpoint}&api-version=1.0', 145 | AUTHORIZE_ENDPOINT_PATH : '/oauth2/authorize', 146 | TOKEN_ENDPOINT_PATH : '/oauth2/token', 147 | DEVICE_ENDPOINT_PATH : '/oauth2/devicecode' 148 | }, 149 | 150 | UserRealm : { 151 | FederationProtocolType : { 152 | WSFederation : 'wstrust', 153 | SAML2 : 'saml20', 154 | Unknown : 'unknown' 155 | }, 156 | 157 | AccountType : { 158 | Federated : 'federated', 159 | Managed : 'managed', 160 | Unknown : 'unknown' 161 | } 162 | }, 163 | 164 | Saml : { 165 | TokenTypeV1 : 'urn:oasis:names:tc:SAML:1.0:assertion', 166 | TokenTypeV2 : 'urn:oasis:names:tc:SAML:2.0:assertion' 167 | }, 168 | 169 | XmlNamespaces : { 170 | wsdl : 'http://schemas.xmlsoap.org/wsdl/', 171 | sp : 'http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702', 172 | sp2005 : 'http://schemas.xmlsoap.org/ws/2005/07/securitypolicy', 173 | wsu : 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', 174 | wsa10 : 'http://www.w3.org/2005/08/addressing', 175 | http : 'http://schemas.microsoft.com/ws/06/2004/policy/http', 176 | soap12 : 'http://schemas.xmlsoap.org/wsdl/soap12/', 177 | wsp : 'http://schemas.xmlsoap.org/ws/2004/09/policy', 178 | s : 'http://www.w3.org/2003/05/soap-envelope', 179 | wsa : 'http://www.w3.org/2005/08/addressing', 180 | wst : 'http://docs.oasis-open.org/ws-sx/ws-trust/200512', 181 | t : 'http://schemas.xmlsoap.org/ws/2005/02/trust' 182 | }, 183 | 184 | Cache : { 185 | HASH_ALGORITHM : 'sha256' 186 | }, 187 | 188 | HttpError : { 189 | UNAUTHORIZED : 401 190 | }, 191 | 192 | AdalIdParameters : { 193 | SKU : 'x-client-SKU', 194 | VERSION : 'x-client-Ver', 195 | OS : 'x-client-OS', 196 | CPU : 'x-client-CPU', 197 | NODE_SKU : 'Node' 198 | }, 199 | 200 | WSTrustVersion : { 201 | UNDEFINED : 'undefined', 202 | WSTRUST13 : 'wstrust13', 203 | WSTRUST2005 : 'wstrust2005' 204 | } 205 | }; 206 | 207 | module.exports = Constants; 208 | -------------------------------------------------------------------------------- /test/wstrust/RSTR2005.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue 5 | 6 | 7 | 2015-07-31T05:45:47.269Z 8 | 2015-07-31T05:50:47.269Z 9 | 10 | 11 | 12 | 13 | 14 | 15 | 2015-07-31T05:45:47.269Z 16 | 2015-07-31T06:45:47.269Z 17 | 18 | 19 | 20 | urn:federation:MicrosoftOnline 21 | 22 | 23 | 24 | 25 | 26 | 27 | urn:federation:MicrosoftOnline 28 | 29 | 30 | 31 | 32 | y7TujD6J60uo1eFddnNJ1g== 33 | 34 | urn:oasis:names:tc:SAML:1.0:cm:bearer 35 | 36 | 37 | 38 | weji@usystech.net 39 | 40 | 41 | y7TujD6J60uo1eFddnNJ1g== 42 | 43 | 44 | 45 | 46 | y7TujD6J60uo1eFddnNJ1g== 47 | 48 | urn:oasis:names:tc:SAML:1.0:cm:bearer 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | wLymjwk2I6HRIg/CqXSTgV5krZI= 63 | 64 | 65 | JF/mUjzlwpYoSSYIqxWRHPxKv3dzYI6b9p+P9cQQwkPQPgrNNY7ty7KC9z31jYK5RYDE8bE7ycE0nCQsAutQGwIeLrYwboKKoNghPzfqxZmreE/IWJ1I0Fub4K/TqgD0DRe8Bll+KMAtQYqcGqOhAPgSNLe1DGX4fs9w30JxSpCYZzXREhDzf274az820me1pmQ2lDW+WlZzaydJ7oDXvAALSL7jQpyzkim3ajoidxnzM35P4arc5coqoTf4xjVMpTcCgqM2wOynRvPM//jKDWHVUrC95uA6fOVY8yAg/qxNHBmZaXxopMDDSG6Rcl1v/Mjsu2waTdqgGuL8FpunIQ== 66 | 67 | 68 | MIIC3DCCAcSgAwIBAgIQN/oe+zrJYaJIOwe4vG7QIDANBgkqhkiG9w0BAQsFADAqMSgwJgYDVQQDEx9BREZTIFNpZ25pbmcgLSBzdHMudXN5c3RlY2gubmV0MB4XDTE1MDMwODAwMjQ0N1oXDTE2MDMwNzAwMjQ0N1owKjEoMCYGA1UEAxMfQURGUyBTaWduaW5nIC0gc3RzLnVzeXN0ZWNoLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMQrDuLjMDqnFKLKUMilA6jBFYvWnn9dIVvuwmohNyNC/NNimad4nHZXx/IdY6SqHcxdt4tY2rXJj9rOt8cPm82Cvgu2i+buxoHxIQUKehRUUKNtjEqtlYODDz+p/fpleIZuZYZwzKX7qsw+ALELSn1C1moi1wOd30P2sCSsRHY093cNJHhqmBPMocRkqXPWXJtRwV3aOkV0JFUu+UPB5OwAGkaw6HygUR4t+t3AFwUdFAKhtgyNT3DRCyntH9+JFx8S7iSRXuumn7HVAndBpysqCwZS1/iQo5tTRr604aCdJSFBhMD/M+K5z1CjGdIRiaoFRuQz+/RBND9VC5kEX8cCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEADU99Uy1elK77RfUGliVCwUNyKBVBUe5++25H4YKzvadtCEAZPtXL6pNG4TGMrPp/ICeGFAckGIF9HrZKV+CnG85v9cyjdb7Ejr5yYc6qRjy1KJi2C2W6wDOoX+FGEmytgyliMsR1+3+O/MalYmCmTf2cxLqLfWZrcrr6nXNlrFtmSOSuUMUCOzaKrlQozcg23327dbhEp7DdLuz8bVh8mbk4lVYYzdPkc7fWn+9pcvoWHKlSQjT/MY2/5puukp5J8QeUa8M/Y8K4r7lEfc+AGmFUP7yFxmLNqnfonxNthbdSJPlofXL3KxqXpEgRw3V8HmhGZPVFdiCyMFNbt35WZA== 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | _f8ce7751-1854-4c97-bb4e-5393d730bb46 77 | 78 | 79 | 80 | 81 | _f8ce7751-1854-4c97-bb4e-5393d730bb46 82 | 83 | 84 | urn:oasis:names:tc:SAML:1.0:assertion 85 | http://schemas.xmlsoap.org/ws/2005/02/trust/Issue 86 | http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey 87 | 88 | 89 | -------------------------------------------------------------------------------- /test/wstrust-request.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 22 | 'use strict'; 23 | /* Directive tells jshint that suite and test are globals defined by mocha */ 24 | /* global suite */ 25 | /* global test */ 26 | 27 | import * as assert from "assert"; 28 | import * as nock from "nock"; 29 | import * as fs from "fs"; 30 | 31 | const util = require('./util/util'); 32 | const cp = util.commonParameters; 33 | const testRequire = util.testRequire; 34 | 35 | const xmldom = require('xmldom'); 36 | const DOMParser = xmldom.DOMParser; 37 | 38 | const WSTrustRequest = testRequire('wstrust-request'); 39 | const WSTrustVersion = testRequire('constants').WSTrustVersion; 40 | 41 | /** 42 | * Tests the WSTrustRequest class that creates and sends a ws-trust RST request. 43 | */ 44 | suite('WSTrustRequest', function () { 45 | var wstrustEndpoint = 'https://test.wstrust.endpoint/'; 46 | 47 | function getMessageIdFromRSTR(body: any) { 48 | var urnPrefix = 'urn:uuid:'; 49 | var pos = body.indexOf(urnPrefix); 50 | if (-1 === pos) { 51 | return; 52 | } 53 | var exampleGuid = '00000000-0000-0000-0000-000000000000'; 54 | var messageIdLength = urnPrefix.length + exampleGuid.length; 55 | 56 | var messageId = body.substr(pos, messageIdLength); 57 | return messageId; 58 | } 59 | 60 | function getDateFromRST(body: any, elementName: string) { 61 | var searchString = elementName + '>'; 62 | var pos = body.indexOf(searchString); 63 | if (-1 === pos) { 64 | return; 65 | } 66 | var exampleDate = '2013-11-21T00:23:48.406Z'; 67 | return body.substr(pos + searchString.length, exampleDate.length); 68 | } 69 | 70 | function replaceDateInTemplate(body: any, rst: string, elementName: string, replaceKey: string) { 71 | var date = getDateFromRST(body, elementName); 72 | if (!date) { 73 | return; 74 | } 75 | return rst.replace(replaceKey, date); 76 | } 77 | 78 | function compareRSTDocs(rst1: string, rst2: string) { 79 | var left = rst1.replace(/\s/g, '').replace(/"/g, '\''); 80 | var right = rst2.replace(/\s/g, '').replace(/"/g, '\''); 81 | 82 | return left === right; 83 | } 84 | 85 | function setupUpOutgoingRSTCompare(rst: any) { 86 | var rstRequest = nock(wstrustEndpoint) 87 | .filteringRequestBody(function (body) { 88 | var messageId = getMessageIdFromRSTR(body); 89 | assert(messageId, 'Could not find message id in return RST'); 90 | rst = rst.replace('%MESSAGE_ID%', messageId); 91 | 92 | rst = replaceDateInTemplate(body, rst, 'Created', '%CREATED%'); 93 | assert(rst, 'Could not find Created date'); 94 | 95 | rst = replaceDateInTemplate(body, rst, 'Expires', '%EXPIRES%'); 96 | assert(rst, 'Could not find Expires date'); 97 | assert(compareRSTDocs(rst, body), 'RST returned does not match expected RST:\n' + body); 98 | return 'OK'; 99 | }) 100 | .post('/', 'OK').reply(200, 'OK'); 101 | 102 | util.matchStandardRequestHeaders(rstRequest); 103 | 104 | return rstRequest; 105 | } 106 | 107 | test('happy-path', function (done) { 108 | var username = 'test_username'; 109 | var password = 'test_password'; 110 | var appliesTo = 'test_appliesTo'; 111 | var templateRST = fs.readFileSync(__dirname + '/wstrust/RST.xml', 'utf8'); 112 | var rst = templateRST.replace('%USERNAME%', username).replace('%PASSWORD%', password).replace('%APPLIES_TO%', appliesTo).replace('%WSTRUST_ENDPOINT%', wstrustEndpoint); 113 | 114 | var rstRequest = setupUpOutgoingRSTCompare(rst); 115 | var request = new WSTrustRequest(cp.callContext, wstrustEndpoint, appliesTo, WSTrustVersion.WSTRUST13); 116 | 117 | // Take over handling the response to short circuit without having WSTrustRequest attmpt 118 | // to proceed with response parsing. 119 | request._handleRSTR = function (body: any, callback: Function) { 120 | body; 121 | callback(); 122 | }; 123 | 124 | request.acquireToken(username, password, function (err: Error) { 125 | rstRequest.done(); 126 | done(err); 127 | }); 128 | }); 129 | 130 | test('happy-path-wstrust2005', function (done) { 131 | var username = 'test_username'; 132 | var password = 'test_password'; 133 | var appliesTo = 'test_appliesTo'; 134 | var templateRST = fs.readFileSync(__dirname + '/wstrust/RST2005.xml', 'utf8'); 135 | var rst = templateRST.replace('%USERNAME%', username).replace('%PASSWORD%', password).replace('%APPLIES_TO%', appliesTo).replace('%WSTRUST_ENDPOINT%', wstrustEndpoint); 136 | 137 | var rstRequest = setupUpOutgoingRSTCompare(rst); 138 | var request = new WSTrustRequest(cp.callContext, wstrustEndpoint, appliesTo, WSTrustVersion.WSTRUST2005); 139 | 140 | request._handleRSTR = function (body: any, callback: Function) { 141 | body; 142 | callback(); 143 | }; 144 | 145 | request.acquireToken(username, password, function (err: Error) { 146 | rstRequest.done(); 147 | done(err); 148 | }); 149 | }); 150 | 151 | test('fail-wstrustversion-undefined', function (done) { 152 | var username = 'test_username'; 153 | var password = 'test_password'; 154 | var appliesTo = 'test_appliesTo'; 155 | var templateRST = fs.readFileSync(__dirname + '/wstrust/RST2005.xml', 'utf8'); 156 | templateRST.replace('%USERNAME%', username).replace('%PASSWORD%', password).replace('%APPLIES_TO%', appliesTo).replace('%WSTRUST_ENDPOINT%', wstrustEndpoint); 157 | 158 | var request = new WSTrustRequest(cp.callContext, wstrustEndpoint, appliesTo, WSTrustVersion.UNDEFINED); 159 | request.acquireToken(username, password, function (err: Error) { 160 | assert(err, 'Did not receive expected error.'); 161 | assert(err.message === 'Unsupported wstrust endpoint version. Current support version is wstrust2005 or wstrust13.'); 162 | done(); 163 | }); 164 | }); 165 | 166 | test('fail-to-parse-rstr', function (done) { 167 | var username = 'test_username'; 168 | var password = 'test_password'; 169 | var appliesTo = 'test_appliesTo'; 170 | var templateRST = fs.readFileSync(__dirname + '/wstrust/RST.xml', 'utf8'); 171 | var rst = templateRST.replace('%USERNAME%', username).replace('%PASSWORD%', password).replace('%APPLIES_TO%', appliesTo).replace('%WSTRUST_ENDPOINT%', wstrustEndpoint); 172 | 173 | var rstRequest = setupUpOutgoingRSTCompare(rst); 174 | var request = new WSTrustRequest(cp.callContext, wstrustEndpoint, appliesTo, WSTrustVersion.WSTRUST13); 175 | 176 | request.acquireToken(username, password, function (err: any) { 177 | rstRequest.done(); 178 | assert(err, 'Did not receive expected error.'); 179 | done(); 180 | }); 181 | }); 182 | 183 | test('xml-format-test', function (done) { 184 | var username = 'test_username'; 185 | var password = 'test_password&<>\'"'; 186 | var appliesTo = 'test_appliesTo'; 187 | 188 | var rst = new WSTrustRequest(cp.callContext, wstrustEndpoint, appliesTo, WSTrustVersion.WSTRUST13)._buildRST(username, password); 189 | 190 | var options = { 191 | errorHandler: function () { throw new Error(); } 192 | }; 193 | 194 | try { 195 | new DOMParser(options).parseFromString(rst); 196 | } 197 | catch (e) { 198 | assert(!e, 'Unexpected error received.'); 199 | } 200 | 201 | done(); 202 | }); 203 | }); 204 | 205 | -------------------------------------------------------------------------------- /lib/log.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var _ = require('underscore'); 24 | var uuid = require('uuid'); // want to replace with this in the future: https://gist.github.com/jed/982883 25 | 26 | 27 | 28 | var LEVEL_STRING_MAP = { 29 | 0 : 'ERROR:', 30 | 1 : 'WARNING:', 31 | 2 : 'INFO:', 32 | 3 : 'VERBOSE:' 33 | }; 34 | 35 | /** 36 | * Methods for controling global logging options for ADAL 37 | * @namespace 38 | */ 39 | var Logging = { 40 | 41 | /** 42 | * @callback LoggingCallback 43 | * @memberOf Logging 44 | * @param {Logging.LOGGING_LEVEL} level The level of this log entry. 45 | * @param {string} message The text content of the log entry. 46 | * @param {Error} [error] An Error object if this is an {@link Logging.LOGGING_LEVEL.ERROR|ERROR} level log entry. 47 | */ 48 | 49 | /** 50 | * @typedef LoggingOptions 51 | * @memberOf Logging 52 | * @property {LoggingCallback} [log] The function to call when ADAL generates a log entry. 53 | * @property {Logging.LOGGING_LEVEL} [level] The maximum level of log entries to generate. 54 | */ 55 | 56 | /** 57 | * Describes the available logging levels. 58 | * @enum 59 | * @type {Number} 60 | */ 61 | LOGGING_LEVEL : { 62 | ERROR : 0, 63 | WARN : 1, 64 | INFO : 2, 65 | VERBOSE : 3 66 | }, 67 | 68 | /** 69 | * Sets global logging options for ADAL. 70 | * @param {LoggingOptions} options 71 | */ 72 | setLoggingOptions : function(options) { 73 | if (!options) { 74 | options = {}; 75 | } 76 | 77 | if (options.log) { 78 | if (!_.isFunction(options.log)) { 79 | throw new Error('setLogOptions expects the log key in the options parameter to be a function'); 80 | } 81 | } else { 82 | // if no log function was passed set it to a default no op function. 83 | options.log = function() {}; 84 | } 85 | 86 | if (options.level) { 87 | var level = options.level; 88 | if (level < 0 || level > 3) { 89 | throw new Error('setLogOptions expects the level key to be in the range 0 to 3 inclusive'); 90 | } 91 | } else { 92 | options.level = this.LOGGING_LEVEL.ERROR; 93 | } 94 | 95 | if (options.loggingWithPII != true) { 96 | options.loggingWithPII = false; 97 | } 98 | 99 | this.LogOptions = options; 100 | }, 101 | 102 | /** 103 | * Get's the current global logging options. 104 | * @return {LoggingOptions} 105 | */ 106 | getLoggingOptions : function() { 107 | return this.LogOptions; 108 | }, 109 | 110 | /** 111 | * Stores the current global logging options. 112 | * @private 113 | * @type {LoggingOptions} 114 | */ 115 | LogOptions : { 116 | log : function() {}, 117 | level : 0, 118 | loggingWithPII: false 119 | } 120 | }; 121 | 122 | /** 123 | * An internal logging object. 124 | * @class 125 | * @private 126 | * @param {string} componentName The name of the component that created this instance. This name will be 127 | * prepended to the beginning of all log entries generated by this instance. 128 | */ 129 | function Logger(componentName, logContext) { 130 | if (!logContext) { 131 | throw new Error('Logger: logContext is a required parameter'); 132 | } 133 | this._componentName = componentName; 134 | this._logContext = logContext; 135 | } 136 | 137 | Object.defineProperty(Logger.prototype, 'context', { 138 | get: function () { 139 | return this._logContext; 140 | } 141 | }); 142 | 143 | /** 144 | * Generates a log entry 145 | * @param {Logging.LOGGING_LEVEL} level The level of this log entry 146 | * @param {string|function} message A message string, or a function that returns a message string, to log. 147 | * @param {Error} [error] If this is a {@link Logging.LOGGING_LEVEL.ERROR|ERROR} level log entry then the caller 148 | * should pass an error object in this parameter. 149 | * @param {boolean} [containsPII] Determines if the log message contains personal information. Default value is false. 150 | */ 151 | Logger.prototype.log = function (level, message, error, containsPII) { 152 | if (containsPII == true && !Logging.LogOptions.loggingWithPII) { 153 | return; 154 | } 155 | 156 | if (level <= Logging.LogOptions.level) { 157 | if (_.isFunction(message)) { 158 | message = message(); 159 | } 160 | 161 | var correlationId = this._logContext.correlationId || ''; 162 | var timeStamp = new Date().toUTCString(); 163 | 164 | var formattedMessage = timeStamp + ':' + correlationId + ' - ' + this._componentName + ': ' + LEVEL_STRING_MAP[level] + ' ' + message; 165 | if (error) { 166 | formattedMessage += '\nStack:\n' + error.stack; 167 | } 168 | Logging.LogOptions.log(level, formattedMessage, error); 169 | } 170 | }; 171 | 172 | /** 173 | * Generate an {@link Logging.LOGGING_LEVEL.ERROR|ERROR} level log entry. 174 | * @param {string} message A message to log 175 | * @param {Error} error The Error object associated with this log entry 176 | * @param {boolean} [containsPII] Determines if the log message contains personal information. Default value is false. 177 | */ 178 | Logger.prototype.error = function (message, error, containsPII) { 179 | this.log(Logging.LOGGING_LEVEL.ERROR, message, error, containsPII); 180 | }; 181 | 182 | /** 183 | * Generate an {@link Logging.LOGGING_LEVEL.WARN|WARN} level log entry. 184 | * @param {string} message A message to log 185 | * @param {boolean} [containsPII] Determines if the log message contains personal information. Default value is false. 186 | */ 187 | Logger.prototype.warn = function (message, containsPII) { 188 | this.log(Logging.LOGGING_LEVEL.WARN, message, null, containsPII); 189 | }; 190 | 191 | /** 192 | * Generate an {@link Logging.LOGGING_LEVEL.INFO|INFO} level log entry. 193 | * @param {string} message A message to log 194 | * @param {boolean} [containsPII] Determines if the log message contains personal information. Default value is false. 195 | */ 196 | Logger.prototype.info = function (message, containsPII) { 197 | this.log(Logging.LOGGING_LEVEL.INFO, message, null, containsPII); 198 | }; 199 | 200 | /** 201 | * Generate an {@link Logging.LOGGING_LEVEL.VERBOSE|VERBOSE} level log entry. 202 | * @param {string} message A message to log 203 | * @param {boolean} [containsPII] Determines if the log message contains personal information. Default value is false. 204 | */ 205 | Logger.prototype.verbose = function (message, containsPII) { 206 | this.log(Logging.LOGGING_LEVEL.VERBOSE, message, null, containsPII); 207 | }; 208 | 209 | /** 210 | * Generate a {@link Logging.LOGGING_LEVEL.ERROR|ERROR} level log entry, as well as an 211 | * Error object to go with it. This is a convenience method for throwing logged errors. 212 | * @param {string} message A message to log 213 | * @param {boolean} [containsPII] Determines if the log message contains personal information. Default value is false. 214 | */ 215 | Logger.prototype.createError = function(message, containsPII) { 216 | var err = new Error(message); 217 | this.error(message, err, containsPII); 218 | return err; 219 | }; 220 | 221 | /** 222 | * Creates a new log context based on the correlationId passed in. If no correlationId is passed in 223 | * then one is generated, by the function uuid.v4() 224 | * @private 225 | */ 226 | function createLogContext(correlationId) { 227 | var id = correlationId || uuid.v4(); 228 | return { correlationId : id }; 229 | } 230 | 231 | var exports = { 232 | Logging : Logging, 233 | Logger : Logger, 234 | createLogContext : createLogContext 235 | }; 236 | 237 | module.exports = exports; -------------------------------------------------------------------------------- /test/wstrust/RSTR.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/RSTRC/IssueFinal 4 | 5 | 6 | 2013-11-15T03:08:25.221Z 7 | 2013-11-15T03:13:25.221Z 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 2013-11-15T03:08:25.205Z 16 | 2013-11-15T04:08:25.205Z 17 | 18 | 19 | 20 | https://login.microsoftonline.com/extSTS.srf 21 | 22 | 23 | 24 | 25 | 26 | 27 | https://login.microsoftonline.com/extSTS.srf 28 | 29 | 30 | 31 | 32 | 1TIu064jGEmmf+hnI+F0Jg== 33 | 34 | urn:oasis:names:tc:SAML:1.0:cm:bearer 35 | 36 | 37 | 38 | frizzo@richard-randall.com 39 | 40 | 41 | 1TIu064jGEmmf+hnI+F0Jg== 42 | 43 | 44 | 45 | 46 | 1TIu064jGEmmf+hnI+F0Jg== 47 | 48 | urn:oasis:names:tc:SAML:1.0:cm:bearer 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 3i95D+nRbsyRitSPeT7ZtEr5vbM= 63 | 64 | 65 | aVNmmKLNdAlBxxcNciWVfxynZUPR9ql8ZZSZt/qpqL/GB3HX/cL/QnfG2OOKrmhgEaR0Ul4grZhGJxlxMPDL0fhnBz+VJ5HwztMFgMYs3Md8A2sZd9n4dfu7+CByAna06lCwwfdFWlNV1MBFvlWvYtCLNkpYVr/aglmb9zpMkNxEOmHe/cwxUtYlzH4RpIsIT5pruoJtUxKcqTRDEeeYdzjBAiJuguQTChLmHNoMPdX1RmtJlPsrZ1s9R/IJky7fHLjB7jiTDceRCS5QUbgUqYbLG1MjFXthY2Hr7K9kpYjxxIk6xmM7mFQE3Hts3bj6UU7ElUvHpX9bxxk3pqzlhg== 66 | 67 | 68 | MIIC6DCCAdCgAwIBAgIQaztYF2TpvZZG6yreA3NRpzANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVBREZTIFNpZ25pbmcgLSBmcy5yaWNoYXJkLXJhbmRhbGwuY29tMB4XDTEzMTExMTAzNTMwMFoXDTE0MTExMTAzNTMwMFowMDEuMCwGA1UEAxMlQURGUyBTaWduaW5nIC0gZnMucmljaGFyZC1yYW5kYWxsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO+1VWY/sYDdN3hdsvT+mWHTcOwjp2G9e0AEZdmgh7bS54WUJw9y0cMxJmGB0jAAW40zomzIbS8/o3iuxcJyFgBVtMFfXwFjVQJnZJ7IMXFs1V/pJHrwWHxePz/WzXFtMaqEIe8QummJ07UBg9UsYZUYTGO9NDGw1Yr/oRNsl7bLA0S/QlW6yryf6l3snHzIgtO2xiWn6q3vCJTTVNMROkI2YKNKdYiD5fFD77kFACfJmOwP8MN9u+HM2IN6g0Nv5s7rMyw077Co/xKefamWQCB0jLpv89jo3hLgkwIgWX4cMVgHSNmdzXSgC3owG8ivRuJDATh83GiqI6jzA1+x4rkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAxA5MQZHw9lJYDpU4f45EYrWPEaAPnncaoxIeLE9fG14gA01frajRfdyoO0AKqb+ZG6sePKngsuq4QHA2EnEI4Di5uWKsXy1Id0AXUSUhLpe63alZ8OwiNKDKn71nwpXnlGwKqljnG3xBMniGtGKrFS4WM+joEHzaKpvgtGRGoDdtXF4UXZJcn2maw6d/kiHrQ3kWoQcQcJ9hVIo8bC0BPvxV0Qh4TF3Nb3tKhaXsY68eMxMGbHok9trVHQ3Vew35FuTg1JzsfCFSDF8sxu7FJ4iZ7VLM8MQLnvIMcubLJvc57EHSsNyeiqBFQIYkdg7MSf+Ot2qJjfExgo+NOtWN+g== 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | _9bd2b280-f153-471a-9b73-c1df0d555075 77 | 78 | 79 | 80 | 81 | _9bd2b280-f153-471a-9b73-c1df0d555075 82 | 83 | 84 | urn:oasis:names:tc:SAML:1.0:assertion 85 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue 86 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | This library, ADAL for Node.js, will no longer receive new feature improvements. Instead, use the new library 4 | [MSAL for Node.js](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-node). 5 | 6 | * If you are starting a new project, you can get started with the 7 | [MSAL for Node.js docs](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-node) 8 | for details about the scenarios, usage, and relevant concepts. 9 | * If your application is using the previous ADAL for Node.js library, you can follow this 10 | [migration guide for Node.js apps](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/migration.md). 11 | * Existing applications relying on ADAL for Node.js will continue to work. 12 | 13 | --- 14 | 15 | | [Feedback](https://forms.office.com/r/er9wCuPXsv) | 16 | |---| 17 | 18 | ## Update to MSAL for Node.js now! 19 | 20 | [MSAL for Node.js](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-node) is the new authentication library to be used with the Microsoft identity platform 21 | 22 | Building on top of ADAL, MSAL works with the new and Open ID Connect certified Azure AD V2 endpoint and the new social identity solution from Microsoft, Azure AD B2C. 23 | 24 | ADAL for Node.js is in maintenance mode and no new features will be added to the ADAL library anymore. All our ongoing efforts will be focused on improving the new [MSAL for Node.js](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-node). MSAL’s documentation also contains a [migration guide](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/migration.md) which simplifies upgrading from ADAL for Node.js. 25 | 26 | # Windows Azure Active Directory Authentication Library (ADAL) for Node.js 27 | The ADAL for node.js library makes it easy for node.js applications to authenticate to AAD in order to access AAD protected web resources. It supports 3 authentication modes shown in the quickstart code below. 28 | 29 | ## Versions 30 | Current version - 0.2.2 31 | Minimum recommended version - 0.2.2 32 | You can find the changes for each version in the [change log](https://github.com/AzureAD/azure-activedirectory-library-for-nodejs/blob/dev/changelog.md). 33 | 34 | ## Samples and Documentation 35 | 36 | [We provide a full suite of sample applications and documentation on GitHub](https://github.com/azure-samples?q=active-directory) to help you get started with learning the Azure Identity system. This includes tutorials for native clients such as Windows, Windows Phone, iOS, OSX, Android, and Linux. We also provide full walkthroughs for authentication flows such as OAuth2, OpenID Connect, Graph API, and other awesome features. 37 | 38 | ## Community Help and Support 39 | 40 | We leverage [Stack Overflow](http://stackoverflow.com/) to work with the community on supporting Azure Active Directory and its SDKs, including this one! We highly recommend you ask your questions on Stack Overflow (we're all on there!) Also browse existing issues to see if someone has had your question before. 41 | 42 | We recommend you use the "adal" tag so we can see it! Here is the latest Q&A on Stack Overflow for ADAL: [http://stackoverflow.com/questions/tagged/adal](http://stackoverflow.com/questions/tagged/adal) 43 | 44 | ## Submit Feedback 45 | We'd like your thoughts on this library. Please complete [this short survey.](https://forms.office.com/r/er9wCuPXsv) 46 | 47 | ## Security Reporting 48 | 49 | If you find a security issue with our libraries or services please report it to [secure@microsoft.com](mailto:secure@microsoft.com) with as much detail as possible. Your submission may be eligible for a bounty through the [Microsoft Bounty](http://aka.ms/bugbounty) program. Please do not post security issues to GitHub Issues or any other public site. We will contact you shortly upon receiving the information. We encourage you to get notifications of when security incidents occur by visiting [this page](https://technet.microsoft.com/en-us/security/dd252948) and subscribing to Security Advisory Alerts. 50 | 51 | ## Contributing 52 | 53 | All code is licensed under the Apache 2.0 license and we triage actively on GitHub. We enthusiastically welcome contributions and feedback. You can clone the repo and start contributing now. 54 | 55 | ## Quick Start 56 | ### Installation 57 | 58 | ``` $ npm install adal-node ``` 59 | 60 | ### Configure the logging 61 | 62 | #### Personal Identifiable Information (PII) & Organizational Identifiable Information (OII) 63 | 64 | By default, ADAL logging does not capture or log any PII or OII. The library allows app developers to turn this on by configuring the `loggingWithPII` flag in the logging options. By turning on PII or OII, the app takes responsibility for safely handling highly-sensitive data and complying with any regulatory requirements. 65 | 66 | ```javascript 67 | var logging = require('adal-node').Logging; 68 | 69 | //PII or OII logging disabled. Default Logger does not capture any PII or OII. 70 | logging.setLoggingOptions({ 71 | log: function(level, message, error) { 72 | // provide your own implementation of the log function 73 | }, 74 | level: logging.LOGGING_LEVEL.VERBOSE, // provide the logging level 75 | loggingWithPII: false // Determine if you want to log personal identification information. The default value is false. 76 | }); 77 | 78 | //PII or OII logging enabled. 79 | logging.setLoggingOptions({ 80 | log: function(level, message, error) { 81 | // provide your own implementation of the log function 82 | }, 83 | level: logging.LOGGING_LEVEL.VERBOSE, 84 | loggingWithPII: true 85 | }); 86 | ``` 87 | 88 | ### Authorization Code 89 | 90 | See the [website sample](https://github.com/MSOpenTech/azure-activedirectory-library-for-nodejs/blob/master/sample/website-sample.js) for a complete bare bones express based web site that makes use of the code below. 91 | 92 | ```javascript 93 | var AuthenticationContext = require('adal-node').AuthenticationContext; 94 | 95 | var clientId = 'yourClientIdHere'; 96 | var clientSecret = 'yourAADIssuedClientSecretHere' 97 | var authorityHostUrl = 'https://login.windows.net'; 98 | var tenant = 'myTenant'; 99 | var authorityUrl = authorityHostUrl + '/' + tenant; 100 | var redirectUri = 'http://localhost:3000/getAToken'; 101 | var resource = '00000002-0000-0000-c000-000000000000'; 102 | var templateAuthzUrl = 'https://login.windows.net/' + 103 | tenant + 104 | '/oauth2/authorize?response_type=code&client_id=' + 105 | clientId + 106 | '&redirect_uri=' + 107 | redirectUri + 108 | '&state=&resource=' + 109 | resource; 110 | 111 | function createAuthorizationUrl(state) { 112 | return templateAuthzUrl.replace('', state); 113 | } 114 | 115 | // Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD. 116 | // There they will authenticate and give their consent to allow this app access to 117 | // some resource they own. 118 | app.get('/auth', function(req, res) { 119 | crypto.randomBytes(48, function(ex, buf) { 120 | var token = buf.toString('base64').replace(/\//g,'_').replace(/\+/g,'-'); 121 | 122 | res.cookie('authstate', token); 123 | var authorizationUrl = createAuthorizationUrl(token); 124 | 125 | res.redirect(authorizationUrl); 126 | }); 127 | }); 128 | 129 | // After consent is granted AAD redirects here. The ADAL library is invoked via the 130 | // AuthenticationContext and retrieves an access token that can be used to access the 131 | // user owned resource. 132 | app.get('/getAToken', function(req, res) { 133 | if (req.cookies.authstate !== req.query.state) { 134 | res.send('error: state does not match'); 135 | } 136 | 137 | var authenticationContext = new AuthenticationContext(authorityUrl); 138 | 139 | authenticationContext.acquireTokenWithAuthorizationCode( 140 | req.query.code, 141 | redirectUri, 142 | resource, 143 | clientId, 144 | clientSecret, 145 | function(err, response) { 146 | var errorMessage = ''; 147 | if (err) { 148 | errorMessage = 'error: ' + err.message + '\n'; 149 | } 150 | errorMessage += 'response: ' + JSON.stringify(response); 151 | res.send(errorMessage); 152 | } 153 | ); 154 | }); 155 | ``` 156 | 157 | ### Server to Server via Client Credentials 158 | 159 | See the [client credentials sample](https://github.com/MSOpenTech/azure-activedirectory-library-for-nodejs/blob/master/sample/client-credentials-sample.js). 160 | 161 | ```javascript 162 | var AuthenticationContext = require('adal-node').AuthenticationContext; 163 | 164 | var authorityHostUrl = 'https://login.windows.net'; 165 | var tenant = 'myTenant.onmicrosoft.com'; // AAD Tenant name. 166 | var authorityUrl = authorityHostUrl + '/' + tenant; 167 | var applicationId = 'yourApplicationIdHere'; // Application Id of app registered under AAD. 168 | var clientSecret = 'yourAADIssuedClientSecretHere'; // Secret generated for app. Read this environment variable. 169 | var resource = '00000002-0000-0000-c000-000000000000'; // URI that identifies the resource for which the token is valid. 170 | 171 | var context = new AuthenticationContext(authorityUrl); 172 | 173 | context.acquireTokenWithClientCredentials(resource, applicationId, clientSecret, function(err, tokenResponse) { 174 | if (err) { 175 | console.log('well that didn\'t work: ' + err.stack); 176 | } else { 177 | console.log(tokenResponse); 178 | } 179 | }); 180 | ``` 181 | 182 | ## License 183 | Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 184 | 185 | ## We Value and Adhere to the Microsoft Open Source Code of Conduct 186 | 187 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 188 | -------------------------------------------------------------------------------- /test/authority.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | /* Directive tells jshint that suite and test are globals defined by mocha */ 23 | /* global suite */ 24 | /* global test */ 25 | /* global setup */ 26 | /* global teardown */ 27 | 28 | import * as assert from "assert"; 29 | 30 | const util = require('./util/util'); 31 | const cp = util.commonParameters; 32 | const testRequire = util.testRequire; 33 | 34 | var adal = testRequire('adal'); 35 | var AuthenticationContext = adal.AuthenticationContext; 36 | var Authority = testRequire('authority').Authority; 37 | 38 | /** 39 | * Tests the Authority class and instance discovery. 40 | */ 41 | suite('Authority', function() { 42 | 43 | setup(function() { 44 | util.resetLogging(); 45 | util.clearStaticCache(); 46 | }); 47 | 48 | teardown(function() { 49 | util.resetLogging(); 50 | util.clearStaticCache(); 51 | }); 52 | 53 | // use this as authority to force dynamic as opposed to static instance discovery. 54 | var nonHardCodedAuthority = 'https://login.doesntexist.com/' + cp.tenant; 55 | var nonHardCodedAuthorizeEndpoint = nonHardCodedAuthority + '/oauth2/authorize'; 56 | 57 | 58 | function setupExpectedInstanceDiscoveryRequestRetries(requestParametersList: any, authority: any) { 59 | var nocks: any[] = []; 60 | 61 | requestParametersList.forEach(function(request: any) { 62 | nocks.push(util.setupExpectedInstanceDiscoveryRequest(request.httpCode, request.authority, request.returnDoc, authority)); 63 | }); 64 | 65 | return nocks; 66 | } 67 | 68 | test('success-dynamic-instance-discovery', function(done) { 69 | var instanceDiscoveryRequest = util.setupExpectedInstanceDiscoveryRequest( 70 | 200, 71 | cp.authorityHosts.global, 72 | { 73 | 'tenant_discovery_endpoint' : 'http://test' 74 | }, 75 | nonHardCodedAuthorizeEndpoint 76 | ); 77 | 78 | var responseOptions = { 79 | authority : nonHardCodedAuthority 80 | }; 81 | var response = util.createResponse(responseOptions); 82 | var wireResponse = response.wireResponse; 83 | var tokenRequest = util.setupExpectedClientCredTokenRequestResponse(200, wireResponse, nonHardCodedAuthority); 84 | 85 | var context = new AuthenticationContext(nonHardCodedAuthority); 86 | context.acquireTokenWithClientCredentials(response.resource, cp.clientId, cp.clientSecret, function (err: any, tokenResponse: any) { 87 | if (!err) { 88 | assert(util.isMatchTokenResponse(response.cachedResponse, tokenResponse), 'The response does not match what was expected.: ' + JSON.stringify(tokenResponse)); 89 | instanceDiscoveryRequest.done(); 90 | tokenRequest.done(); 91 | } 92 | done(err); 93 | }); 94 | }); 95 | 96 | function performStaticInstanceDiscovery(authorityHost: string, callback: Function) { 97 | var hardCodedAuthority = 'https://' + authorityHost + '/' + cp.tenant; 98 | 99 | var responseOptions = { 100 | authority : hardCodedAuthority 101 | }; 102 | var response = util.createResponse(responseOptions); 103 | var wireResponse = response.wireResponse; 104 | var tokenRequest = util.setupExpectedClientCredTokenRequestResponse(200, wireResponse, hardCodedAuthority); 105 | 106 | var context = new AuthenticationContext(hardCodedAuthority); 107 | context.acquireTokenWithClientCredentials(response.resource, cp.clientId, cp.clientSecret, function (err: any, tokenResponse: any) { 108 | if (!err) { 109 | assert(util.isMatchTokenResponse(response.cachedResponse, tokenResponse), 'The response does not match what was expected.: ' + JSON.stringify(tokenResponse)); 110 | tokenRequest.done(); 111 | } 112 | callback(err); 113 | }); 114 | } 115 | 116 | test('success-static-instance-discovery', function(done) { 117 | performStaticInstanceDiscovery('login.windows.net', function(err: any) { 118 | if(err) { 119 | done(err); 120 | return; 121 | } 122 | performStaticInstanceDiscovery('login.microsoftonline.com', function(err2: any) { 123 | if(err2) { 124 | done(err2); 125 | return; 126 | } 127 | performStaticInstanceDiscovery('login.chinacloudapi.cn', function(err3: any) { 128 | if(err3) { 129 | done(err3); 130 | return; 131 | } 132 | performStaticInstanceDiscovery('login-us.microsoftonline.com', function(err4: any) { 133 | if(err4) { 134 | done(err4); 135 | return; 136 | } 137 | performStaticInstanceDiscovery('login.microsoftonline.us', function(err5: any) { 138 | done(err5); 139 | }) 140 | }); 141 | }); 142 | }); 143 | }); 144 | }); 145 | 146 | test('http-error', function(done) { 147 | var expectedInstanceDiscoveryRequests = [ 148 | { 149 | httpCode : 500, 150 | authority : cp.authorityHosts.global 151 | //returnDoc : null 152 | } 153 | ]; 154 | 155 | var instanceDiscoveryRequests = setupExpectedInstanceDiscoveryRequestRetries(expectedInstanceDiscoveryRequests, nonHardCodedAuthorizeEndpoint); 156 | 157 | var context = new AuthenticationContext(nonHardCodedAuthority); 158 | context.acquireTokenWithClientCredentials(cp.resource, cp.clientId, cp.clientSecret, function (err: any) { 159 | assert(err, 'No error was returned when one was expected.'); 160 | assert(err.message.indexOf('500') !== -1, 'The http error was not returned'); 161 | instanceDiscoveryRequests.forEach(function(request){ 162 | request.done(); 163 | }); 164 | 165 | done(); 166 | }); 167 | }); 168 | 169 | test('validation-error', function(done) { 170 | var expectedInstanceDiscoveryRequests = [ 171 | { 172 | httpCode : 400, 173 | authority : cp.authorityHosts.global, 174 | returnDoc : { error : 'invalid_instance', 'error_description' : 'the instance was invalid' } 175 | } 176 | ]; 177 | 178 | var instanceDiscoveryRequests = setupExpectedInstanceDiscoveryRequestRetries(expectedInstanceDiscoveryRequests, nonHardCodedAuthorizeEndpoint); 179 | 180 | var context = new AuthenticationContext(nonHardCodedAuthority); 181 | context.acquireTokenWithClientCredentials(cp.resource, cp.clientId, cp.clientSecret, function (err: any) { 182 | assert(err, 'No error was returned when one was expected.'); 183 | assert(err.message.indexOf('invalid_instance') !== -1, 'The server error was not returned'); 184 | assert(err.message.indexOf('instance was invalid') !== -1, 'The server error message was not returned'); 185 | instanceDiscoveryRequests.forEach(function(request){ 186 | request.done(); 187 | }); 188 | 189 | done(); 190 | }); 191 | }); 192 | 193 | test('validation-off', function(done) { 194 | var response = util.createResponse(); 195 | var wireResponse = response.wireResponse; 196 | var tokenRequest = util.setupExpectedClientCredTokenRequestResponse(200, wireResponse, response.authority); 197 | 198 | var context = new AuthenticationContext(cp.authorityTenant, false); 199 | context.acquireTokenWithClientCredentials(response.resource, cp.clientId, cp.clientSecret, function (err: any, tokenResponse: any) { 200 | if (!err) { 201 | assert(util.isMatchTokenResponse(response.cachedResponse, tokenResponse), 'The response does not match what was expected.'); 202 | tokenRequest.done(); 203 | } 204 | done(err); 205 | }); 206 | }); 207 | 208 | test('bad-url-not-https', function(done) { 209 | var errorThrown; 210 | try { 211 | new AuthenticationContext('http://this.is.not.https.com/mytenant.com'); 212 | } catch(err) { 213 | errorThrown = err; 214 | } 215 | 216 | assert(errorThrown, 'AuthenticationContext succeeded when it should have failed.'); 217 | assert(errorThrown.message.indexOf('https') >= 0, 'Error message does not mention the need for https: ' + errorThrown.message); 218 | done(); 219 | }); 220 | 221 | test('bad-url-has-query', function(done) { 222 | var errorThrown: any; 223 | try { 224 | new AuthenticationContext(cp.authorityTenant + '?this=should¬=be&here=foo'); 225 | } catch(err) { 226 | errorThrown = err; 227 | } 228 | 229 | assert(errorThrown, 'AuthenticationContext succeeded when it should have failed.'); 230 | assert(errorThrown.message.indexOf('query') >= 0, 'Error message does not mention the offending query string: ' + errorThrown.message); 231 | done(); 232 | }); 233 | 234 | test('url-extra-path-elements', function(done) { 235 | var instanceDiscoveryRequest = util.setupExpectedInstanceDiscoveryRequest( 236 | 200, 237 | cp.authorityHosts.global, 238 | { 239 | 'tenant_discovery_endpoint' : 'http://test' 240 | }, 241 | nonHardCodedAuthorizeEndpoint 242 | ); 243 | 244 | // add extra path and query string to end of the authority. These should be stripped 245 | // out before the url is sent to instance discovery. 246 | var authorityUrl = nonHardCodedAuthority + '/extra/path'; 247 | var authority = new Authority(authorityUrl, true); 248 | var obj = util.createEmptyADALObject(); 249 | authority.validate(obj._callContext, function(err: any) { 250 | if (err) { 251 | assert(!err, 'Received unexpected error: ' + err.stack); 252 | } 253 | 254 | assert(authority.tokenEndpoint === (nonHardCodedAuthority + cp.tokenPath), "oauth2 token endpoint should be after tenant in the url"); 255 | assert(authority.deviceCodeEndpoint === (nonHardCodedAuthority + cp.deviceCodePath), "oauth2 device endpoint should be after tenant in the url"); 256 | instanceDiscoveryRequest.done(); 257 | done(); 258 | }); 259 | }); 260 | }); 261 | -------------------------------------------------------------------------------- /lib/user-realm.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var querystring = require('querystring'); 24 | var axios = require('axios'); 25 | var _ = require('underscore'); 26 | var url = require('url'); 27 | 28 | var constants = require('./constants'); 29 | var Logger = require('./log').Logger; 30 | var util = require('./util'); 31 | 32 | var AccountType = constants.UserRealm.AccountType; 33 | var FederationProtocolType = constants.UserRealm.FederationProtocolType; 34 | 35 | var USER_REALM_PATH_TEMPLATE = 'common/UserRealm/'; 36 | 37 | /** 38 | * Create a new UserRealm object 39 | * @private 40 | * @constructor 41 | * @param {object} callContext Contains any context information that applies to the request. 42 | * @param {string} userPrinciple The username for which a realm needs to be discovered. 43 | * @param {string} authority The string url of the authority that owns the userPrinciple. 44 | */ 45 | function UserRealm(callContext, userPrinciple, authority) { 46 | this._log = new Logger('UserRealm', callContext._logContext); 47 | this._callContext = callContext; 48 | this._apiVersion = '1.0'; 49 | this._federationProtocol = null; 50 | this._accountType = null; 51 | this._federationMetadataUrl = null; 52 | this._federationActiveAuthUrl = null; 53 | this._userPrinciple = userPrinciple; 54 | this._authority = authority; 55 | } 56 | 57 | /** 58 | * The API version requested by UserRealm. 59 | * @type {string} 60 | * @instance 61 | * @memberOf UserRealm 62 | * @name apiVersion 63 | */ 64 | Object.defineProperty(UserRealm.prototype, 'apiVersion', { 65 | get : function() { 66 | return this._apiVersion; 67 | } 68 | }); 69 | 70 | /** 71 | * The federation protocol used by the users realm. 72 | * @type {string} 73 | * @instance 74 | * @memberOf UserRealm 75 | * @name federationProtocol 76 | */ 77 | Object.defineProperty(UserRealm.prototype, 'federationProtocol', { 78 | get : function() { 79 | return this._federationProtocol; 80 | } 81 | }); 82 | 83 | /** 84 | * The Type of account. Either managed or federated. 85 | * @type {string} 86 | * @instance 87 | * @memberOf UserRealm 88 | * @name accountType 89 | */ 90 | Object.defineProperty(UserRealm.prototype, 'accountType', { 91 | get : function() { 92 | return this._accountType; 93 | } 94 | }); 95 | 96 | /** 97 | * If this is a federated account then this property will contain the mex url. 98 | * @type {string} 99 | * @instance 100 | * @memberOf UserRealm 101 | * @name federationsMetadataUrl 102 | */ 103 | Object.defineProperty(UserRealm.prototype, 'federationMetadataUrl', { 104 | get : function() { 105 | return this._federationMetadataUrl; 106 | } 107 | }); 108 | 109 | /** 110 | * If the account is federated this will contain the authentication endpoint. 111 | * @type {string} 112 | * @instance 113 | * @memberOf UserRealm 114 | * @name federationActiveAuthUrl 115 | */ 116 | Object.defineProperty(UserRealm.prototype, 'federationActiveAuthUrl', { 117 | get : function() { 118 | return this._federationActiveAuthUrl; 119 | } 120 | }); 121 | 122 | /** 123 | * Given the authority url this method constructs a full user realm discovery url. 124 | * @private 125 | * @returns A full user realm discovery url including path and query string. 126 | */ 127 | UserRealm.prototype._getUserRealmUrl = function() { 128 | var userRealmUrl = util.copyUrl(this._authority); 129 | var urlEncodedUser = encodeURIComponent(this._userPrinciple); 130 | userRealmUrl.pathname = USER_REALM_PATH_TEMPLATE.replace('', urlEncodedUser); 131 | 132 | var userRealmQuery = { 133 | 'api-version' : this._apiVersion 134 | }; 135 | 136 | userRealmUrl.search = querystring.stringify(userRealmQuery); 137 | 138 | userRealmUrl = util.copyUrl(userRealmUrl); 139 | 140 | return userRealmUrl; 141 | }; 142 | 143 | /** 144 | * Given a constants object and a value, validates that the value is a key in the constants object. 145 | * @private 146 | * @param {object} constants An object containing constant key value pairs. 147 | * @param {string} value A value to check against the constants 148 | * @param {bool} caseSensitive set to true if comparisons should be made as case sensitive. Defaults to false. 149 | * @returns {bool|string} If value passed in matches one of the constants then the return value is the matched constant. 150 | * If a non case sensitive match was done, then the value returned may be different than the value 151 | * passed in. If there is no match then the method returns false. 152 | */ 153 | UserRealm.prototype._validateConstantValue = function(constants, value, caseSensitive) { 154 | if (!value) { 155 | return false; 156 | } 157 | if (!caseSensitive) { 158 | value = value.toLowerCase(); 159 | } 160 | return _.contains(_.values(constants), value) ? value : false; 161 | }; 162 | 163 | /** 164 | * Checks whether an account type string is valid. 165 | * @private 166 | * @param {string} type An account type string. 167 | * @returns {bool} 168 | */ 169 | UserRealm.prototype._validateAccountType = function(type) { 170 | return this._validateConstantValue(AccountType, type); 171 | }; 172 | 173 | /** 174 | * Checks whether a federation protocol string is valid. 175 | * @private 176 | * @param {string} protocol A federation protocol string. 177 | * @returns {bool} 178 | */ 179 | UserRealm.prototype._validateFederationProtocol = function(protocol) { 180 | return this._validateConstantValue(FederationProtocolType, protocol); 181 | }; 182 | 183 | /** 184 | * Logs the values parsed as part of user realm discovery. 185 | * @private 186 | */ 187 | UserRealm.prototype._logParsedResponse = function() { 188 | this._log.verbose('UserRealm response:'); 189 | this._log.verbose(' AccountType: ' + this.accountType); 190 | this._log.verbose(' FederationProtocol: ' + this.federationProtocol); 191 | this._log.verbose(' FederationMetatdataUrl: ' + this.federationMetadataUrl, true); 192 | this._log.verbose(' FederationActiveAuthUrl: ' + this.federationActiveAuthUrl, true); 193 | }; 194 | 195 | /** 196 | * Parses the response from a user realm discovery request. 197 | * @private 198 | * @param {string} body The body returned as part of the http user realm discovery request. 199 | * @param {UserRealm.DiscoverCallback} callback Called when parsing is complete. 200 | */ 201 | UserRealm.prototype._parseDiscoveryResponse = function(body, callback) { 202 | this._log.verbose('Discovery response:\n' + body, true); 203 | 204 | var response; 205 | try { 206 | response = body; 207 | } catch (err) { 208 | callback(this._log.createError('Parsing realm discovery respone JSON failed: ' + body, true)); 209 | return; 210 | } 211 | 212 | var accountType = this._validateAccountType(response['account_type']); 213 | if (!accountType) { 214 | callback(this._log.createError('Cannot parse account_type: ' + accountType)); 215 | return; 216 | } 217 | 218 | this._accountType = accountType; 219 | 220 | if (this._accountType === AccountType.Federated) { 221 | var protocol = this._validateFederationProtocol(response['federation_protocol']); 222 | 223 | if (!protocol) { 224 | callback(this._log.createError('Cannot parse federation protocol: ' + protocol)); 225 | return; 226 | } 227 | 228 | this._federationProtocol = protocol; 229 | this._federationMetadataUrl = response['federation_metadata_url']; 230 | this._federationActiveAuthUrl = response['federation_active_auth_url']; 231 | } 232 | 233 | this._logParsedResponse(); 234 | callback(); 235 | }; 236 | 237 | /** 238 | * @callback DiscoverCallback 239 | * @memberOf UserRealm 240 | * @param {Error} error If an error occurs during discovery then this parameter will be used to return the error. 241 | */ 242 | 243 | /** 244 | * Performs user realm discovery and fills in the properties on this object. 245 | * @private 246 | * @param {UserRealm.DiscoverCallback} callback Called when discovery is complete. 247 | */ 248 | UserRealm.prototype.discover = function(callback) { 249 | var self = this; 250 | var options = util.createRequestOptions( 251 | this, 252 | { 253 | headers : { 254 | Accept : 'application/json' 255 | } 256 | } 257 | ); 258 | 259 | var userRealmUrl = this._getUserRealmUrl(); 260 | this._log.verbose('Performing user realm discovery at: ' + url.format(userRealmUrl), true); 261 | 262 | axios.get(userRealmUrl, options).then((response) => { 263 | util.logReturnCorrelationId(this._log, 'User Realm Discovery', response); 264 | // status >= 300 && < 400 265 | if (!util.isHttpSuccess(response.status)) { 266 | var returnErrorString = 'User Realm Discovery' + ' request returned http error: ' + response.status + ' and server response: ' + response.status; 267 | callback(this._log.createError(returnErrorString, true), response.data); 268 | } 269 | // Success case: status >= 200 && < 300 270 | self._parseDiscoveryResponse(response.data, callback); 271 | }).catch((error) => { 272 | // status >= 400: error case 273 | if (error.response) { 274 | util.logReturnCorrelationId(this._log, 'User Realm Discovery', error.response); 275 | this._log.error('User Realm Discovery' + ' request failed with', error.response.status, true); 276 | var returnErrorString = 'User Realm Discovery' + ' request returned http error: ' + error.response.status + ' and server response: ' + JSON.stringify(error.response.data); 277 | callback(this._log.createError(returnErrorString, true), error.response.data); 278 | } 279 | // if there is no response from the server 280 | else if (error.request) { 281 | this._log.error('User Realm Discovery' + ' request was made but no response was received', error.request, true); 282 | callback(self._log.createError('No response from the server')); 283 | } 284 | // request was never made 285 | else { 286 | this._log.error('User Realm Discovery' + ' request was never made, please check', error.message, true); 287 | callback(error.message); 288 | } 289 | }); 290 | }; 291 | 292 | module.exports = UserRealm; -------------------------------------------------------------------------------- /lib/wstrust-response.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var xmldom = require('xmldom'); 24 | 25 | var xmlutil = require('./xmlutil'); 26 | 27 | var Logger = require('./log').Logger; 28 | 29 | var WSTrustVersion = require('./constants').WSTrustVersion; 30 | 31 | var select = xmlutil.xpathSelect; 32 | var DOMParser = xmldom.DOMParser; 33 | 34 | // A regular expression for finding the SAML Assertion in an RSTR. Used to remove the SAML 35 | // assertion when logging the RSTR. 36 | var assertionRegEx = /RequestedSecurityToken.*?((<.*?:Assertion.*?>).*<\/.*?Assertion>).*?/; 37 | 38 | /** 39 | * Creates a log message that contains the RSTR scrubbed of the actual SAML assertion. 40 | * @private 41 | * @return {string} A log message. 42 | */ 43 | function scrubRSTRLogMessage(RSTR) { 44 | var scrubbedRSTR = null; 45 | 46 | var singleLineRSTR = RSTR.replace(/(\r\n|\n|\r)/gm,''); 47 | 48 | var matchResult = assertionRegEx.exec(singleLineRSTR); 49 | if (null === matchResult) { 50 | // No Assertion was matched so just return the RSTR as is. 51 | scrubbedRSTR = singleLineRSTR; 52 | } else { 53 | var samlAssertion = matchResult[1]; 54 | var samlAssertionStartTag = matchResult[2]; 55 | 56 | scrubbedRSTR = singleLineRSTR.replace(samlAssertion, samlAssertionStartTag + 'ASSERTION CONTENTS REDACTED'); 57 | } 58 | 59 | return 'RSTR Response: ' + scrubbedRSTR; 60 | } 61 | 62 | /** 63 | * Creates a new WSTrustResponse instance. 64 | * @constructor 65 | * @private 66 | * @param {object} callContext Contains any context information that applies to the request. 67 | * @param {string} response A soap response from a WS-Trust request. 68 | * @param {sting} wstrustVersion The version for the WS-Trust request. 69 | */ 70 | function WSTrustResponse(callContext, response, wstrustVersion) { 71 | this._log = new Logger('WSTrustResponse', callContext._logContext); 72 | this._callContext = callContext; 73 | this._response = response; 74 | this._dom = null; 75 | this._errorCode = null; 76 | this._faultMessage = null; 77 | this._tokenType = null; 78 | this._token = null; 79 | this._wstrustVersion = wstrustVersion; 80 | 81 | this._log.verbose(function(){return scrubRSTRLogMessage(response);}); 82 | } 83 | 84 | /** 85 | * If the soap response contained a soap fault then this property will contain the fault 86 | * error code. Otherwise it will return null 87 | * @instance 88 | * @type {string} 89 | * @memberOf WSTrustResponse 90 | * @name errorCode 91 | */ 92 | Object.defineProperty(WSTrustResponse.prototype, 'errorCode', { 93 | get: function() { 94 | return this._errorCode; 95 | } 96 | }); 97 | 98 | /** 99 | * @property {string} FaultMessage If the soap resopnse contained a soap fault with a fault message then it will 100 | * be returned by this property. 101 | * @instance 102 | * @type {string} 103 | * @memberOf WSTrustResponse 104 | * @name faultMessage 105 | */ 106 | Object.defineProperty(WSTrustResponse.prototype, 'faultMessage', { 107 | get: function() { 108 | return this._faultMessage; 109 | } 110 | }); 111 | 112 | /** 113 | * @property {string} TokenType If the soap resonse contained a token then this property will contain 114 | * the token type uri 115 | * @instance 116 | * @type {string} 117 | * @memberOf WSTrustResponse 118 | * @name tokenType 119 | */ 120 | Object.defineProperty(WSTrustResponse.prototype, 'tokenType', { 121 | get: function() { 122 | return this._tokenType; 123 | } 124 | }); 125 | 126 | /** 127 | * @property {string} Token If the soap response contained a token then this property will hold that token. 128 | * @instance 129 | * @type {string} 130 | * @memberOf WSTrustResponse 131 | * @name token 132 | */ 133 | Object.defineProperty(WSTrustResponse.prototype, 'token', { 134 | get: function() { 135 | return this._token; 136 | } 137 | }); 138 | 139 | // Sample error message 140 | // 141 | // 142 | // http://www.w3.org/2005/08/addressing/soap/fault 143 | // - 144 | // 145 | // 2013-07-30T00:32:21.989Z 146 | // 2013-07-30T00:37:21.989Z 147 | // 148 | // 149 | // 150 | // 151 | // 152 | // 153 | // s:Sender 154 | // 155 | // a:RequestFailed 156 | // 157 | // 158 | // 159 | // MSIS3127: The specified request failed. 160 | // 161 | // 162 | // 163 | // 164 | 165 | /** 166 | * Attempts to parse an error from the soap response. If there is one then it 167 | * will fill in the error related properties. Otherwsie it will do nothing. 168 | * @private 169 | * @returns {bool} true if an error was found and parsed in the response. 170 | */ 171 | WSTrustResponse.prototype._parseError = function() { 172 | var errorFound = false; 173 | 174 | var faultNode = select(this._dom, '//s:Envelope/s:Body/s:Fault/s:Reason'); 175 | if (faultNode.length) { 176 | this._faultMessage = xmlutil.serializeNodeChildren(faultNode[0]); 177 | 178 | if (this._faultMessage) { 179 | errorFound = true; 180 | } 181 | } 182 | 183 | // Subcode has minoccurs=0 and maxoccurs=1(default) according to the http://www.w3.org/2003/05/soap-envelope 184 | // Subcode may have another subcode as well. This is only targetting at top level subcode. 185 | // Subcode value may have different messages not always uses http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd. 186 | // text inside the value is not possible to select without prefix, so substring is necessary 187 | var subcodeNode = select(this._dom, '//s:Envelope/s:Body/s:Fault/s:Code/s:Subcode/s:Value'); 188 | if (1 < subcodeNode.length) { 189 | throw this._log.createError('Found too many fault code values:' + subcodeNode.length); 190 | } 191 | 192 | if (subcodeNode.length) { 193 | var errorCode = subcodeNode[0].firstChild.data; 194 | this._errorCode = (errorCode.split(':'))[1]; 195 | errorFound = true; 196 | } 197 | 198 | return errorFound; 199 | }; 200 | 201 | /** 202 | * Attempts to parse a token from the soap response. If there is one then it will fill in the 203 | * token related properties. Otherwise it does nothing. 204 | * @private 205 | * @throws {Error} If the response is not parseable, or too many tokens are found. 206 | */ 207 | WSTrustResponse.prototype._parseToken = function() { 208 | var xPath = this._wstrustVersion === WSTrustVersion.WSTRUST2005 ? '//s:Envelope/s:Body/t:RequestSecurityTokenResponse/t:TokenType' : '//s:Envelope/s:Body/wst:RequestSecurityTokenResponseCollection/wst:RequestSecurityTokenResponse/wst:TokenType'; 209 | 210 | var tokenTypeNodes = select(this._dom, xPath); 211 | if (!tokenTypeNodes.length) { 212 | this._log.warn('No TokenType elements found in RSTR'); 213 | } 214 | 215 | for (var i = 0, length = tokenTypeNodes.length; i < length; i++) { 216 | if (this._token) { 217 | this._log.warn('Found more than one returned token. Using the first.'); 218 | break; 219 | } 220 | 221 | var tokenTypeNode = tokenTypeNodes[i]; 222 | var tokenType = xmlutil.findElementText(tokenTypeNode); 223 | if (!tokenType) { 224 | this._log.warn('Could not find token type in RSTR token'); 225 | } 226 | 227 | var securityTokenPath = this._wstrustVersion === WSTrustVersion.WSTRUST2005 ? 't:RequestedSecurityToken' : 'wst:RequestedSecurityToken'; 228 | var requestedTokenNode = select(tokenTypeNode.parentNode, securityTokenPath); 229 | if (1 < requestedTokenNode) { 230 | throw this._log.createError('Found too many RequestedSecurityToken nodes for token type: ' + tokenType); 231 | } 232 | if (!requestedTokenNode.length) { 233 | this._log.warn('Unable to find RequestsSecurityToken element associated with TokenType element: ' + tokenType); 234 | continue; 235 | } 236 | 237 | var token = xmlutil.serializeNodeChildren(requestedTokenNode[0]); 238 | if (!token) { 239 | this._log.warn('Unable to find token associated with TokenType element: ' + tokenType); 240 | continue; 241 | } 242 | 243 | this._token = token; 244 | this._tokenType = tokenType; 245 | 246 | this._log.info('Found token of type: ' + this._tokenType); 247 | } 248 | 249 | if (!this._token) { 250 | throw this._log.createError('Unable to find any tokens in RSTR.'); 251 | } 252 | }; 253 | 254 | /** 255 | * This method parses the soap response that was passed in at construction. 256 | * @throws {Error} If the server returned an error, or there was any failure to parse the response. 257 | */ 258 | WSTrustResponse.prototype.parse = function() { 259 | if (!this._response) { 260 | throw this._log.createError('Received empty RSTR response body.'); 261 | } 262 | 263 | try { 264 | try { 265 | var options = { 266 | errorHandler : this._log.error 267 | }; 268 | this._dom = new DOMParser(options).parseFromString(this._response); 269 | } catch (err) { 270 | throw this._log.createError('Failed to parse RSTR in to DOM', err, true); 271 | } 272 | 273 | var errorFound = this._parseError(); 274 | 275 | if (errorFound) { 276 | var stringErrorCode = this.ErrorCode || 'NONE'; 277 | var stringFaultMessage = this.FaultMessage || 'NONE'; 278 | throw this._log.createError('Server returned error in RSTR - ErrorCode: ' + stringErrorCode + ' : FaultMessage: ' + stringFaultMessage, true); 279 | } 280 | 281 | this._parseToken(); 282 | } catch (err) { 283 | delete this._dom; 284 | throw err; 285 | } 286 | }; 287 | 288 | module.exports = WSTrustResponse; -------------------------------------------------------------------------------- /lib/wstrust-request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @copyright 3 | * Copyright © Microsoft Open Technologies, Inc. 4 | * 5 | * All Rights Reserved 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http: *www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS 14 | * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 15 | * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A 16 | * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT. 17 | * 18 | * See the Apache License, Version 2.0 for the specific language 19 | * governing permissions and limitations under the License. 20 | */ 21 | 'use strict'; 22 | 23 | var axios = require('axios'); 24 | var uuid = require('uuid'); 25 | 26 | var Logger = require('./log').Logger; 27 | var util = require('./util'); 28 | var WSTrustResponse = require('./wstrust-response'); 29 | var WSTrustVersion = require('./constants').WSTrustVersion; 30 | 31 | var USERNAME_PLACEHOLDER = '{UsernamePlaceHolder}'; 32 | var PASSWORD_PLACEHOLDER = '{PasswordPlaceHolder}'; 33 | 34 | /** 35 | * Creates a new instance of WSTrustRequest 36 | * @constructor 37 | * @private 38 | * @param {object} callContext Contains any context information that applies to the request. 39 | * @param {string} wstrustEndpointUrl An STS WS-Trust soap endpoint. 40 | * @param {string} appliesTo A URI that identifies a service for which the a token is to be obtained. 41 | */ 42 | function WSTrustRequest(callContext, wstrustEndpointUrl, appliesTo, wstrustEndpointVersion) { 43 | this._log = new Logger('WSTrustRequest', callContext._logContext); 44 | this._callContext = callContext; 45 | this._wstrustEndpointUrl = wstrustEndpointUrl; 46 | this._appliesTo = appliesTo; 47 | this._wstrustEndpointVersion = wstrustEndpointVersion; 48 | } 49 | 50 | /** 51 | * Given a Date object adds the minutes parameter and returns a new Date object. 52 | * @private 53 | * @static 54 | * @memberOf WSTrustRequest 55 | * @param {Date} date A Date object. 56 | * @param {Number} minutes The number of minutes to add to the date parameter. 57 | * @returns {Date} Returns a Date object. 58 | */ 59 | function _datePlusMinutes(date, minutes) { 60 | var minutesInMilliSeconds = minutes * 60 * 1000; 61 | var epochTime = date.getTime() + minutesInMilliSeconds; 62 | return new Date(epochTime); 63 | } 64 | 65 | /** 66 | * Builds the soap security header for the RST message. 67 | * @private 68 | * @param {string} username A username 69 | * @param {string} password The passowrd that corresponds to the username parameter. 70 | * @returns {string} A string that contains the soap security header. 71 | */ 72 | WSTrustRequest.prototype._buildSecurityHeader = function() { 73 | var timeNow = new Date(); 74 | var expireTime = _datePlusMinutes(timeNow, 10); 75 | var timeNowString = timeNow.toISOString(); 76 | var expireTimeString = expireTime.toISOString(); 77 | 78 | var securityHeaderXml = 79 | '\ 80 | \ 81 | ' + timeNowString + '\ 82 | ' + expireTimeString + '\ 83 | \ 84 | \ 85 | ' + USERNAME_PLACEHOLDER + '\ 86 | ' + PASSWORD_PLACEHOLDER + '\ 87 | \ 88 | '; 89 | 90 | return securityHeaderXml; 91 | }; 92 | 93 | /** 94 | * Replaces the placeholders in the RST template with the actual username and password values. 95 | * @private 96 | * @param {string} RSTTemplate An RST with placeholders for username and password. 97 | * @param {string} username A username 98 | * @param {string} password The passowrd that corresponds to the username parameter. 99 | * @returns {string} A string containing a complete RST soap message. 100 | */ 101 | 102 | WSTrustRequest.prototype._populateRSTUsernamePassword = function(RSTTemplate, username, password) { 103 | var RST = RSTTemplate.replace(USERNAME_PLACEHOLDER, username).replace(PASSWORD_PLACEHOLDER, this._populatedEscapedPassword(password)); 104 | return RST; 105 | }; 106 | 107 | /** 108 | * Escape xml characters in password. 109 | * @private 110 | * @param {string} password The password to be excaped with xml charaters. 111 | */ 112 | WSTrustRequest.prototype._populatedEscapedPassword = function (password) { 113 | var escapedPassword = password; 114 | return escapedPassword.replace(/&/g, '&') 115 | .replace(/"/g, '"') 116 | .replace(/'/g, ''') 117 | .replace(//g, '>'); 119 | } 120 | 121 | /** 122 | * Builds a WS-Trust RequestSecurityToken (RST) message using username password authentication. 123 | * @private 124 | * @param {string} username A username 125 | * @param {string} password The passowrd that corresponds to the username parameter. 126 | * @returns {string} A string containing a complete RST soap message. 127 | */ 128 | WSTrustRequest.prototype._buildRST = function(username, password) { 129 | var messageID = uuid.v4(); 130 | 131 | // Create a template RST with placeholders for the username and password so the 132 | // the RST can be logged without the sensitive information. 133 | var schemaLocation = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'; 134 | var soapAction = 'http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue'; 135 | var rstTrustNamespace = 'http://docs.oasis-open.org/ws-sx/ws-trust/200512'; 136 | var keyType = 'http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer'; 137 | var requestType = 'http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue'; 138 | 139 | if (this._wstrustEndpointVersion === WSTrustVersion.WSTRUST2005) { 140 | soapAction = 'http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue'; 141 | rstTrustNamespace = 'http://schemas.xmlsoap.org/ws/2005/02/trust'; 142 | keyType = 'http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey'; 143 | requestType = 'http://schemas.xmlsoap.org/ws/2005/02/trust/Issue'; 144 | } 145 | 146 | var RSTTemplate = 147 | '\ 148 | \ 149 | ' + soapAction + '\ 150 | urn:uuid:' + messageID + '\ 151 | \ 152 | http://www.w3.org/2005/08/addressing/anonymous\ 153 | \ 154 | ' + this._wstrustEndpointUrl + '\ 155 | ' + this._buildSecurityHeader() + '\ 156 | \ 157 | \ 158 | \ 159 | \ 160 | \ 161 | ' + this._appliesTo + '\ 162 | \ 163 | \ 164 | ' + keyType + '\ 165 | ' + requestType + '\ 166 | \ 167 | \ 168 | '; 169 | 170 | this._log.verbose('Created RST: \n' + RSTTemplate, true); 171 | 172 | var RST = this._populateRSTUsernamePassword(RSTTemplate, username, password); 173 | return RST; 174 | }; 175 | 176 | /** 177 | * Handles the processing of a RSTR 178 | * @private 179 | * @param {string} body 180 | * @param {WSTrustRequest.AcquireTokenCallback} callback 181 | */ 182 | WSTrustRequest.prototype._handleRSTR = function(body, callback) { 183 | var err; 184 | 185 | var wstrustResponse = new WSTrustResponse(this._callContext, body, this._wstrustEndpointVersion); 186 | try { 187 | wstrustResponse.parse(); 188 | } catch (error) { 189 | err = error; 190 | } 191 | 192 | callback(err, wstrustResponse); 193 | }; 194 | 195 | /** 196 | * Performs a WS-Trust RequestSecurityToken request to obtain a federated token in exchange for a username password. 197 | * @param {string} username A username 198 | * @param {string} password The passowrd that corresponds to the username parameter. 199 | * @param {WSTrustRequest.AcquireTokenCallback} callback Called once the federated token has been retrieved or on error. 200 | */ 201 | WSTrustRequest.prototype.acquireToken = function(username, password, callback) { 202 | if (this._wstrustEndpointVersion === WSTrustVersion.UNDEFINED) { 203 | var err = this._log.createError('Unsupported wstrust endpoint version. Current support version is wstrust2005 or wstrust13.'); 204 | callback(err); 205 | return; 206 | } 207 | 208 | var self = this; 209 | var RST = this._buildRST(username, password); 210 | 211 | var soapAction = this._wstrustEndpointVersion === WSTrustVersion.WSTRUST2005 ? 'http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue' : 'http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue'; 212 | 213 | var options = util.createRequestOptions( 214 | this, 215 | { 216 | method: 'POST', 217 | url: this._wstrustEndpointUrl, 218 | headers : { 219 | 'Content-Type' : 'application/soap+xml; charset=utf-8', 220 | 'SOAPAction' : soapAction 221 | }, 222 | data: RST, 223 | maxRedirects: 0, 224 | encoding: 'utf8', 225 | } 226 | ); 227 | 228 | this._log.verbose('Sending RST to: ' + this._wstrustEndpointUrl, true); 229 | 230 | axios(options).then((response) => { 231 | util.logReturnCorrelationId(this._log, 'WS-Trust RST', response); 232 | // status >= 300 && < 400 233 | if (!util.isHttpSuccess(response.status)) { 234 | var returnErrorString = 'WS-Trust RST' + ' request returned http error: ' + response.status + ' and server response: ' + JSON.stringify(error.response.data); 235 | callback(this._log.createError(returnErrorString, true), response.data); 236 | } 237 | // Success case: status >= 200 && < 300 238 | self._handleRSTR(response.data, callback); 239 | }).catch((error) => { 240 | // status >= 400: error case 241 | if (error.response) { 242 | util.logReturnCorrelationId(this._log, 'WS-Trust RST', error.response); 243 | this._log.error('WS-Trust RST' + ' request failed with', error.response.status, true); 244 | var returnErrorString = 'WS-Trust RST' + ' request returned http error: ' + error.response.status + ' and server response: ' + JSON.stringify(error.response.data); 245 | callback(self._log.createError(returnErrorString, true), error.response.data); 246 | } 247 | // if there is no response from the server 248 | else if (error.request) { 249 | this._log.error('WS-Trust RST' + ' request was made but no response was received', error.request, true); 250 | callback(self._log.createError('No response from the server')); 251 | } 252 | // request was never made 253 | else if (error.message) { 254 | this._log.error('WS-Trust RST' + ' request was never made, please check', error.message, true); 255 | callback(error.message); 256 | } 257 | // unknown error 258 | else { 259 | self._log.error('WS-Trust RST' + ' failed with unknown error', "unknown error", true); 260 | callback(self._log.createError('failed with an unknown error')); 261 | } 262 | }); 263 | }; 264 | 265 | /** 266 | * @callback AcquireTokenCallback 267 | * @memberOf WSTrustRequest 268 | * @param {Error} err Contains an error object if acquireToken fails. 269 | * @param {WSTrustResponse} A successful response to the RST. 270 | */ 271 | 272 | module.exports = WSTrustRequest; 273 | --------------------------------------------------------------------------------