├── .editorconfig ├── .eslintrc.json ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── Consts.ts ├── auth │ ├── AuthResolverFactory.ts │ ├── HostingEnvironment.ts │ ├── IAuthOptions.ts │ ├── IAuthResolver.ts │ ├── IAuthResponse.ts │ ├── base │ │ ├── IAccessToken.ts │ │ ├── IAppToken.ts │ │ ├── IAuthData.ts │ │ └── index.ts │ └── resolvers │ │ ├── AdfsCredentials.ts │ │ ├── FileConfig.ts │ │ ├── OnDemand │ │ └── OnDemand.ts │ │ ├── OnlineAddinOnly.ts │ │ ├── OnlineUserCredentials.ts │ │ ├── OnpremiseAddinOnly.ts │ │ ├── OnpremiseFbaCredentials.ts │ │ ├── OnpremiseTmgCredentials.ts │ │ ├── OnpremiseUserCredentials.ts │ │ ├── base │ │ └── OnlineResolver.ts │ │ └── ondemand │ │ └── electron │ │ ├── main.js │ │ └── no-ntlm.html ├── config.ts ├── index.ts ├── templates │ ├── AdfsSamlToken.ts │ ├── AdfsSamlWsfed.ts │ ├── FbaLoginWsfed.ts │ ├── OnlineSamlWsfed.ts │ └── OnlineSamlWsfedAdfs.ts └── utils │ ├── AdfsHelper.ts │ ├── Cache.ts │ ├── CacheItem.ts │ ├── FilesHelper.ts │ ├── SamlAssertion.ts │ ├── TokenHelper.ts │ └── UrlHelper.ts ├── test └── integration │ ├── config.sample.ts │ ├── integration.spec.ts │ ├── spaddin.key │ └── tests.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-explicit-any": "off", 14 | "@typescript-eslint/interface-name-prefix": "off", 15 | "quotes": [2, "single"], 16 | "@typescript-eslint/explicit-module-boundary-types": "off" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | When submitting a pull request, make sure you do the following: 2 | 3 | - submit the PR to the `dev` branch on the main repo, **not** the `master` branch 4 | - in the body, reference the issue that it closes by number in the format `Closes #000` 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/integration/config.ts 3 | lib/ 4 | coverage/ 5 | .nyc_output/ 6 | demo -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | /reports 3 | /coverage 4 | /.nyc_output 5 | /.github 6 | test 7 | /build 8 | /typings 9 | .vscode 10 | *.js.map 11 | !/lib -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run mocha integration", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 9 | "stopOnEntry": false, 10 | "args": [ 11 | "-r", 12 | "ts-node/register", 13 | "test/integration/tests.ts" 14 | ],/* 15 | "env": { 16 | "http_proxy": "http://127.0.0.1:8888" 17 | },*/ 18 | "cwd": "${workspaceRoot}", 19 | "protocol": "inspector" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.renderWhitespace": "all", 3 | "editor.tabSize": 2, 4 | "files.trimTrailingWhitespace": false, 5 | "files.exclude": { 6 | "node_modules/": true, 7 | ".nyc_output/": true, 8 | "**/*.js": { 9 | "when": "$(basename).ts" 10 | }, 11 | "**/*.d.ts": { 12 | "when": "$(basename).js" 13 | }, 14 | "**/*.js.map": true 15 | }, 16 | "editor.codeActionsOnSave": { 17 | "source.fixAll": "explicit" 18 | }, 19 | "editor.wordWrap": "on", 20 | "files.autoSave": "onFocusChange", 21 | "editor.minimap.maxColumn": 90, 22 | "editor.minimap.renderCharacters": false, 23 | "typescript.disableAutomaticTypeAcquisition": true 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "test:integration", 9 | "problemMatcher": [] 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sergei Sergeev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-sp-auth - nodejs to SharePoint unattended http authentication 2 | 3 | [![NPM](https://nodei.co/npm/node-sp-auth.png?mini=true)](https://nodei.co/npm/node-sp-auth/) 4 | [![npm version](https://badge.fury.io/js/node-sp-auth.svg)](https://badge.fury.io/js/node-sp-auth) 5 | 6 | --- 7 | > [!CAUTION] 8 | > 9 | > I don't use this module for many years and don't have time to actively maintain it. Thus no new versions are expected and no new features. Only your PR requests, if they are valid. 10 | > 11 | > Also, with Azure ACS and SharePoint Add-in model [retirement](https://techcommunity.microsoft.com/t5/microsoft-sharepoint-blog/sharepoint-add-in-retirement-in-microsoft-365/ba-p/3982035) some authentication methods for SharePoint Online will stop working after April, 2026. 12 | > 13 | > I strongly recommend to use [msal-node](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-node) for authentication purposes. 14 | --- 15 | `node-sp-auth` allows you to perform SharePoint unattended (without user interaction) http authentication with nodejs using different authentication techniques. `node-sp-auth` also takes care about caching authentication data for performance (no need for you to think about how long authentication will be available, that's a task for `node-sp-auth`, as soon as authentication will be expired, `node-sp-auth` will renew it internally). 16 | 17 | Versions supported: 18 | 19 | * SharePoint 2013 and onwards 20 | * SharePoint Online 21 | 22 | Authentication options: 23 | 24 | * SharePoint 2013 and onwards: 25 | * Addin only permissions 26 | * User credentials through the http ntlm handshake 27 | * Form-based authentication (FBA) 28 | * Forefront TMG authentication 29 | * SharePoint Online: 30 | * Addin only permissions 31 | * SAML based with user credentials 32 | * ADFS user credentials (works with both SharePoint on-premise and Online) 33 | * On demand authentication. Uses interactive browser session for asking credentials. Supports third-party authentication providers for SharePoint Online and SharePoint on-premise. Doesn't support integrated windows authentication (NTLM). 34 | 35 | [Wiki](https://github.com/s-KaiNet/node-sp-auth/wiki) contains detailed steps you need to perform in order to use any of authentication options as well as sample using. 36 | 37 | --- 38 | 39 | ### How to use 40 | 41 | #### Install 42 | 43 | ```bash 44 | npm install node-sp-auth --save-dev 45 | ``` 46 | 47 | #### Create authentication headers and perform http request 48 | 49 | ```javascript 50 | import * as spauth from 'node-sp-auth'; 51 | import * as request from 'request-promise'; 52 | 53 | //get auth options 54 | spauth.getAuth(url, credentialOptions) 55 | .then(options => { 56 | 57 | //perform request with any http-enabled library (request-promise in a sample below): 58 | let headers = options.headers; 59 | headers['Accept'] = 'application/json;odata=verbose'; 60 | 61 | request.get({ 62 | url: 'https://[your tenant].sharepoint.com/sites/dev/_api/web', 63 | headers: headers 64 | }).then(response => { 65 | //process data 66 | }); 67 | }); 68 | ``` 69 | 70 | ## API 71 | 72 | ### getAuth(url, credentialOptions) 73 | 74 | #### return value 75 | 76 | Promise resolving into object with following properties: 77 | 78 | * `headers` - http headers (normally contain `Authorization` header, may contain any other heraders as well) 79 | * `options` - any additional options you may need to include for succesful request. For example, in case of on premise user credentials authentication, you need to set `agent` property on corresponding http client 80 | 81 | #### params 82 | 83 | * `url` - required, string, url to SharePoint site, `https://sp2013/sites/dev/` or `https://[your tenant].sharepoint.com/sites/dev/` 84 | * `credentialOptions` - optional, object in a form of key-value. Each authentication option requires predefined credential object, depending on authentication type. Based on credentials provided, `node-sp-auth` automatically determines which authentication strategy to use (strategies listed in the top of the readme file). 85 | 86 | Possible values for `credentialOptions` (depending on authentication strategy): 87 | 88 | * SharePoint on premise (2013, 2016): 89 | * [Addin only permissions:](https://github.com/s-KaiNet/node-sp-auth/wiki/SharePoint%20on-premise%20addin%20only%20authentication) 90 | `clientId`, `issuerId`, `realm`, `rsaPrivateKeyPath`, `shaThumbprint` 91 | * [User credentials through the http ntlm handshake:](https://github.com/s-KaiNet/node-sp-auth/wiki/SharePoint%20on-premise%20user%20credentials%20authentication) 92 | `username`, `password`, `domain`, `workstation` 93 | * [User credentials for form-based authentication (FBA):](https://github.com/s-KaiNet/node-sp-auth/wiki/SharePoint%20on-premise%20FBA%20authentication) 94 | `username`, `password`, `fba` = true 95 | * User credentials for Forefront TMG (reverse proxy): 96 | `username`, `password`, `tmg` = true 97 | 98 | * SharePoint Online: 99 | * [Addin only permissions:](https://github.com/s-KaiNet/node-sp-auth/wiki/SharePoint%20Online%20addin%20only%20authentication) 100 | `clientId`, `clientSecret` 101 | * [SAML based with user credentials](https://github.com/s-KaiNet/node-sp-auth/wiki/SharePoint%20Online%20user%20credentials%20authentication) 102 | `username` , `password`, `online` 103 | 104 | * [ADFS user credentials:](https://github.com/s-KaiNet/node-sp-auth/wiki/ADFS%20user%20credentials%20authentication) 105 | `username`, `password`, `relyingParty`, `adfsUrl`, `adfsCookie` 106 | * [On demand authentication](https://github.com/s-KaiNet/node-sp-auth/wiki/On%20demand%20authentication) 107 | `ondemand` = true, `electron`, `force`, `persist`, `ttl` 108 | * no authentication - do not provide any authentication data at all, like `spauth.getAuth(url).then(...)`. In that case `node-sp-auth` will ask you for the site url and credentials. You will have to select any of the credential options listed above. Credentials will be stored in a user folder in an encrypted manner. 109 | Credits: Andrew Koltyakov [@koltyakov](https://github.com/koltyakov) and his awesome [node-sp-auth-config](https://github.com/koltyakov/node-sp-auth-config) 110 | 111 | Please, use [Wiki](https://github.com/s-KaiNet/node-sp-auth/wiki/) to see how you can configure your environment in order to use any of this authentication options. 112 | 113 | ### setup(configuration) 114 | 115 | #### params 116 | 117 | * `configuration` - object accepting some configuration values for node-sp-auth. Currently it supports only configuration of underline `request` module via providing below code (for options available consider [request repository](https://github.com/request/request#requestoptions-callback)): 118 | 119 | ```typescript 120 | spauth.setup({ 121 | requestOptions: {... request options object} 122 | }); 123 | ``` 124 | 125 | ## Development 126 | 127 | I recommend using VS Code for development. Repository already contains some settings for VS Code editor. 128 | 129 | Before creating Pull Request you need to create an appropriate issue and reference it from PR. 130 | 131 | 1. `git clone https://github.com/s-KaiNet/node-sp-auth.git` 132 | 2. `npm run build` - runs linting and typescript compilation 133 | 3. `npm run dev` - setup watchers and automatically runs typescript compilation, tslint and tests when you save files 134 | 135 | ## Integration testing 136 | 137 | 1. Rename file `/test/integration/config.sample.ts` to `config.ts`. 138 | 2. Update information in `config.ts` with appropriate values (urls, credentials). 139 | 3. Run `npm run test:integration`. 140 | 4. For tests debugging put a breakpoint and press F5 (works in VSCode only). 141 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3.0.9", 3 | "name": "node-sp-auth", 4 | "author": "Sergei Sergeev (https://github.com/s-KaiNet)", 5 | "description": "Unattended SharePoint http authentication with nodejs", 6 | "main": "./lib/src/index.js", 7 | "typings": "./lib/src/index", 8 | "keywords": [ 9 | "sharepoint", 10 | "authentication", 11 | "nodejs", 12 | "saml", 13 | "oauth", 14 | "adfs" 15 | ], 16 | "bugs": { 17 | "url": "https://github.com/s-KaiNet/node-sp-auth/issues" 18 | }, 19 | "homepage": "https://github.com/s-KaiNet/node-sp-auth", 20 | "scripts": { 21 | "build": "npm run lint && tsc -p . && npm run copy", 22 | "copy": "cpy src/auth/resolvers/ondemand/electron/*.* lib/src/auth/resolvers/ondemand/electron", 23 | "dev": "npm run copy && tsc -p . --watch", 24 | "lint": "eslint -c .eslintrc.json --ext .ts src test", 25 | "prepublishOnly": "rimraf -- lib && npm run build", 26 | "test:dev": "mocha ./test/integration/tests.ts --watch --watch-extensions ts", 27 | "coverage": "nyc mocha ./test/integration/tests.ts" 28 | }, 29 | "nyc": { 30 | "extends": "@istanbuljs/nyc-config-typescript", 31 | "all": true, 32 | "report-dir": "coverage/integration", 33 | "include": [ 34 | "src/**/*.ts" 35 | ], 36 | "extension": [ 37 | ".ts" 38 | ], 39 | "reporter": [ 40 | "html", 41 | "text-summary" 42 | ] 43 | }, 44 | "mocha": { 45 | "require": [ 46 | "ts-node/register", 47 | "source-map-support/register" 48 | ], 49 | "full-trace": true, 50 | "bail": true 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "git://github.com/s-KaiNet/node-sp-auth.git" 55 | }, 56 | "devDependencies": { 57 | "@istanbuljs/nyc-config-typescript": "1.0.1", 58 | "@types/chai": "4.2.11", 59 | "@types/mocha": "7.0.2", 60 | "@typescript-eslint/eslint-plugin": "^3.3.0", 61 | "@typescript-eslint/parser": "^3.3.0", 62 | "chai": "4.2.0", 63 | "cpy-cli": "^4.1.0", 64 | "eslint": "^7.3.0", 65 | "mocha": "^8.4.0", 66 | "nyc": "15.1.0", 67 | "rimraf": "3.0.2", 68 | "source-map-support": "0.5.19", 69 | "ts-node": "8.10.2", 70 | "typescript": "3.9.5" 71 | }, 72 | "dependencies": { 73 | "@types/cookie": "0.4.0", 74 | "@types/core-js": "2.5.3", 75 | "@types/global-agent": "2.1.0", 76 | "@types/jsonwebtoken": "8.5.0", 77 | "@types/lodash.template": "4.5.0", 78 | "@types/node": "14.0.13", 79 | "cookie": "0.4.1", 80 | "cpass": "2.3.0", 81 | "global-agent": "2.1.12", 82 | "got": "10.7.0", 83 | "jsonwebtoken": "8.5.1", 84 | "lodash.template": "4.5.0", 85 | "node-ntlm-client": "0.1.2", 86 | "node-sp-auth-config": "3.0.1", 87 | "xmldoc": "1.1.2" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Consts.ts: -------------------------------------------------------------------------------- 1 | export const SharePointServicePrincipal = '00000003-0000-0ff1-ce00-000000000000'; 2 | export const HighTrustTokenLifeTime: number = 12 * 60 * 60; 3 | export const FbaAuthEndpoint = '_vti_bin/authentication.asmx'; 4 | export const TmgAuthEndpoint = 'CookieAuth.dll?Logon'; 5 | export const FormsPath = '_forms/default.aspx?wa=wsignin1.0'; 6 | export const RtFa = 'rtFa'; 7 | export const FedAuth = 'FedAuth'; 8 | export const AdfsOnlineRealm = 'urn:federation:MicrosoftOnline'; 9 | -------------------------------------------------------------------------------- /src/auth/AuthResolverFactory.ts: -------------------------------------------------------------------------------- 1 | import { IAuthResolver } from './IAuthResolver'; 2 | import { OnpremiseFbaCredentials } from './resolvers/OnpremiseFbaCredentials'; 3 | import { OnpremiseTmgCredentials } from './resolvers/OnpremiseTmgCredentials'; 4 | import { OnpremiseUserCredentials } from './resolvers/OnpremiseUserCredentials'; 5 | import { OnlineUserCredentials } from './resolvers/OnlineUserCredentials'; 6 | import { OnlineAddinOnly } from './resolvers/OnlineAddinOnly'; 7 | import { OnpremiseAddinOnly } from './resolvers/OnpremiseAddinOnly'; 8 | import { AdfsCredentials } from './resolvers/AdfsCredentials'; 9 | import { OnDemand } from './resolvers/OnDemand/OnDemand'; 10 | import * as authOptions from './IAuthOptions'; 11 | import { FileConfig } from './resolvers/FileConfig'; 12 | 13 | export class AuthResolverFactory { 14 | public static resolve(siteUrl: string, options?: authOptions.IAuthOptions): IAuthResolver { 15 | 16 | if (!options) { 17 | return new FileConfig(siteUrl); 18 | } 19 | 20 | if (authOptions.isTmgCredentialsOnpremise(siteUrl, options)) { 21 | return new OnpremiseTmgCredentials(siteUrl, options); 22 | } 23 | 24 | if (authOptions.isFbaCredentialsOnpremise(siteUrl, options)) { 25 | return new OnpremiseFbaCredentials(siteUrl, options); 26 | } 27 | 28 | if (authOptions.isUserCredentialsOnpremise(siteUrl, options)) { 29 | return new OnpremiseUserCredentials(siteUrl, options); 30 | } 31 | 32 | if (authOptions.isUserCredentialsOnline(siteUrl, options)) { 33 | return new OnlineUserCredentials(siteUrl, options); 34 | } 35 | 36 | if (authOptions.isAddinOnlyOnline(options)) { 37 | return new OnlineAddinOnly(siteUrl, options); 38 | } 39 | 40 | if (authOptions.isAddinOnlyOnpremise(options)) { 41 | return new OnpremiseAddinOnly(siteUrl, options); 42 | } 43 | 44 | if (authOptions.isAdfsCredentials(options)) { 45 | return new AdfsCredentials(siteUrl, options); 46 | } 47 | 48 | if (authOptions.isOndemandCredentials(options)) { 49 | return new OnDemand(siteUrl, options); 50 | } 51 | 52 | throw new Error('Error while resolving authentication class'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/auth/HostingEnvironment.ts: -------------------------------------------------------------------------------- 1 | export enum HostingEnvironment { 2 | Production = 0, 3 | German = 1, 4 | China = 2, 5 | USGovernment = 3, 6 | USDefence = 4 7 | } 8 | -------------------------------------------------------------------------------- /src/auth/IAuthOptions.ts: -------------------------------------------------------------------------------- 1 | import * as url from 'url'; 2 | 3 | export interface IBasicOAuthOption { 4 | clientId: string; 5 | } 6 | 7 | export interface IOnlineAddinCredentials extends IBasicOAuthOption { 8 | clientSecret: string; 9 | realm?: string; 10 | } 11 | 12 | export interface IOnPremiseAddinCredentials extends IBasicOAuthOption { 13 | realm: string; 14 | issuerId: string; 15 | rsaPrivateKeyPath: string; 16 | shaThumbprint: string; 17 | } 18 | 19 | export interface IUserCredentials { 20 | username: string; 21 | password: string; 22 | online?: boolean; 23 | } 24 | 25 | export interface IOnpremiseTmgCredentials extends IUserCredentials { 26 | tmg: boolean; 27 | } 28 | 29 | export interface IOnpremiseFbaCredentials extends IUserCredentials { 30 | fba: boolean; 31 | } 32 | 33 | export interface IOnpremiseUserCredentials extends IUserCredentials { 34 | domain?: string; 35 | workstation?: string; 36 | rejectUnauthorized?: boolean; 37 | } 38 | 39 | export interface IAdfsUserCredentials extends IUserCredentials { 40 | domain?: string; 41 | adfsCookie?: string; 42 | adfsUrl: string; 43 | relyingParty: string; 44 | } 45 | 46 | export interface IOnDemandCredentials { 47 | ondemand: boolean; 48 | electron?: string; 49 | force?: boolean; 50 | persist?: boolean; 51 | ttl?: number; // session TTL in minutes 52 | } 53 | 54 | export type IAuthOptions = 55 | IOnlineAddinCredentials 56 | | IOnPremiseAddinCredentials 57 | | IUserCredentials 58 | | IOnpremiseUserCredentials 59 | | IAdfsUserCredentials 60 | | IOnDemandCredentials; 61 | 62 | export function isOnPremUrl(siteUrl: string): boolean { 63 | const host: string = (url.parse(siteUrl)).host; 64 | return host.indexOf('.sharepoint.com') === -1 && host.indexOf('.sharepoint.cn') === -1 && host.indexOf('.sharepoint.de') === -1 65 | && host.indexOf('.sharepoint-mil.us') === -1 && host.indexOf('.sharepoint.us') === -1; 66 | } 67 | 68 | export function isAddinOnlyOnline(T: IAuthOptions): T is IOnlineAddinCredentials { 69 | return (T as IOnlineAddinCredentials).clientSecret !== undefined; 70 | } 71 | 72 | export function isAddinOnlyOnpremise(T: IAuthOptions): T is IOnPremiseAddinCredentials { 73 | return (T as IOnPremiseAddinCredentials).shaThumbprint !== undefined; 74 | } 75 | 76 | export function isUserCredentialsOnline(siteUrl: string, T: IAuthOptions): T is IUserCredentials { 77 | if ((T as IUserCredentials).online) { 78 | return true; 79 | } 80 | 81 | const isOnPrem: boolean = isOnPremUrl(siteUrl); 82 | 83 | if (!isOnPrem && (T as IUserCredentials).username !== undefined && !isAdfsCredentials(T)) { 84 | return true; 85 | } 86 | 87 | return false; 88 | } 89 | 90 | export function isUserCredentialsOnpremise(siteUrl: string, T: IAuthOptions): T is IOnpremiseUserCredentials { 91 | if ((T as IUserCredentials).online) { 92 | return false; 93 | } 94 | 95 | const isOnPrem: boolean = isOnPremUrl(siteUrl); 96 | 97 | if (isOnPrem && (T as IUserCredentials).username !== undefined && !isAdfsCredentials(T)) { 98 | return true; 99 | } 100 | 101 | return false; 102 | } 103 | 104 | export function isTmgCredentialsOnpremise(siteUrl: string, T: IAuthOptions): T is IOnpremiseTmgCredentials { 105 | const isOnPrem: boolean = isOnPremUrl(siteUrl); 106 | 107 | if (isOnPrem && (T as IOnpremiseFbaCredentials).username !== undefined && (T as IOnpremiseTmgCredentials).tmg) { 108 | return true; 109 | } 110 | 111 | return false; 112 | } 113 | 114 | export function isFbaCredentialsOnpremise(siteUrl: string, T: IAuthOptions): T is IOnpremiseFbaCredentials { 115 | const isOnPrem: boolean = isOnPremUrl(siteUrl); 116 | 117 | if (isOnPrem && (T as IOnpremiseFbaCredentials).username !== undefined && (T as IOnpremiseFbaCredentials).fba) { 118 | return true; 119 | } 120 | 121 | return false; 122 | } 123 | 124 | export function isAdfsCredentials(T: IAuthOptions): T is IAdfsUserCredentials { 125 | return (T as IAdfsUserCredentials).adfsUrl !== undefined; 126 | } 127 | 128 | export function isOndemandCredentials(T: IAuthOptions): T is IOnDemandCredentials { 129 | return (T as IOnDemandCredentials).ondemand !== undefined; 130 | } 131 | -------------------------------------------------------------------------------- /src/auth/IAuthResolver.ts: -------------------------------------------------------------------------------- 1 | import { IAuthResponse } from './IAuthResponse'; 2 | 3 | export interface IAuthResolver { 4 | getAuth: () => Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/auth/IAuthResponse.ts: -------------------------------------------------------------------------------- 1 | export interface IAuthResponse { 2 | headers: { [key: string]: any }; 3 | options?: { [key: string]: any }; 4 | } 5 | -------------------------------------------------------------------------------- /src/auth/base/IAccessToken.ts: -------------------------------------------------------------------------------- 1 | export interface IAccessToken { 2 | value: string; 3 | expireOn: Date; 4 | } 5 | -------------------------------------------------------------------------------- /src/auth/base/IAppToken.ts: -------------------------------------------------------------------------------- 1 | export interface IAppToken { 2 | appctx: string; 3 | appctxsender: string; 4 | aud: string; 5 | exp: number; 6 | iat: number; 7 | nbf: number; 8 | iss: string; 9 | refreshtoken: string; 10 | realm: string; 11 | context: {CacheKey: string; SecurityTokenServiceUri: string}; 12 | } 13 | -------------------------------------------------------------------------------- /src/auth/base/IAuthData.ts: -------------------------------------------------------------------------------- 1 | export interface IAuthData { 2 | refreshToken: string; 3 | realm: string; 4 | securityTokenServiceUri: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/auth/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './IAccessToken'; 2 | export * from './IAuthData'; 3 | export * from './IAppToken'; 4 | -------------------------------------------------------------------------------- /src/auth/resolvers/AdfsCredentials.ts: -------------------------------------------------------------------------------- 1 | import { request } from './../../config'; 2 | import * as url from 'url'; 3 | import * as cookie from 'cookie'; 4 | import template = require('lodash.template'); 5 | import { Response } from 'got'; 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const xmldoc: any = require('xmldoc'); 8 | 9 | import { IAuthResolver } from './../IAuthResolver'; 10 | import { IAdfsUserCredentials } from './../IAuthOptions'; 11 | import { IAuthResponse } from './../IAuthResponse'; 12 | import { Cache } from './../../utils/Cache'; 13 | import * as consts from './../../Consts'; 14 | import { AdfsHelper } from './../../utils/AdfsHelper'; 15 | import { SamlAssertion } from './../../utils/SamlAssertion'; 16 | 17 | import { template as adfsSamlTokenTemplate } from './../../templates/AdfsSamlToken'; 18 | 19 | export class AdfsCredentials implements IAuthResolver { 20 | 21 | private static CookieCache: Cache = new Cache(); 22 | private _authOptions: IAdfsUserCredentials; 23 | 24 | constructor(private _siteUrl: string, _authOptions: IAdfsUserCredentials) { 25 | this._authOptions = Object.assign({}, _authOptions); 26 | 27 | this._authOptions.username = this._authOptions.username 28 | .replace(/&/g, '&') 29 | .replace(/"/g, '"') 30 | .replace(/'/g, ''') 31 | .replace(//g, '>'); 33 | 34 | this._authOptions.password = this._authOptions.password 35 | .replace(/&/g, '&') 36 | .replace(/"/g, '"') 37 | .replace(/'/g, ''') 38 | .replace(//g, '>'); 40 | 41 | if (this._authOptions.domain !== undefined) { 42 | this._authOptions.username = `${this._authOptions.domain}\\${this._authOptions.username}`; 43 | } 44 | } 45 | 46 | public getAuth(): Promise { 47 | const siteUrlParsed: url.Url = url.parse(this._siteUrl); 48 | 49 | const cacheKey = `${siteUrlParsed.host}@${this._authOptions.username}@${this._authOptions.password}`; 50 | const cachedCookie: string = AdfsCredentials.CookieCache.get(cacheKey); 51 | 52 | if (cachedCookie) { 53 | return Promise.resolve({ 54 | headers: { 55 | 'Cookie': cachedCookie 56 | } 57 | }); 58 | } 59 | 60 | return AdfsHelper.getSamlAssertion(this._authOptions) 61 | .then((data: any) => { 62 | return this.postTokenData(data); 63 | }) 64 | .then(data => { 65 | const adfsCookie: string = this._authOptions.adfsCookie || consts.FedAuth; 66 | const notAfter: number = new Date(data[0]).getTime(); 67 | const expiresIn: number = parseInt(((notAfter - new Date().getTime()) / 1000).toString(), 10); 68 | const response = data[1]; 69 | 70 | const authCookie: string = adfsCookie + '=' + 71 | response.headers['set-cookie'] 72 | .map((cookieString: string) => cookie.parse(cookieString)[adfsCookie]) 73 | .filter((cookieString: string) => typeof cookieString !== 'undefined')[0]; 74 | 75 | AdfsCredentials.CookieCache.set(cacheKey, authCookie, expiresIn); 76 | 77 | return { 78 | headers: { 79 | 'Cookie': authCookie 80 | } 81 | }; 82 | }); 83 | } 84 | 85 | private postTokenData(samlAssertion: SamlAssertion): Promise<[string, Response]> { 86 | const result: string = template(adfsSamlTokenTemplate)({ 87 | created: samlAssertion.notBefore, 88 | expires: samlAssertion.notAfter, 89 | relyingParty: this._authOptions.relyingParty, 90 | token: samlAssertion.value 91 | }); 92 | 93 | const tokenXmlDoc: any = new xmldoc.XmlDocument(result); 94 | const siteUrlParsed: url.Url = url.parse(this._siteUrl); 95 | const rootSiteUrl = `${siteUrlParsed.protocol}//${siteUrlParsed.host}`; 96 | 97 | return Promise.all([samlAssertion.notAfter, request.post(`${rootSiteUrl}/_trust/`, { 98 | form: { 99 | 'wa': 'wsignin1.0', 100 | 'wctx': `${rootSiteUrl}/_layouts/Authenticate.aspx?Source=%2F`, 101 | 'wresult': tokenXmlDoc.toString({ compressed: true }) 102 | } 103 | })]); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/auth/resolvers/FileConfig.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import { AuthConfig } from 'node-sp-auth-config'; 5 | import { IAuthResolver } from '../IAuthResolver'; 6 | import { IAuthResponse } from '../IAuthResponse'; 7 | import { FilesHelper } from '../../utils/FilesHelper'; 8 | import { AuthResolverFactory } from './../AuthResolverFactory'; 9 | import { Cache } from './../../utils/Cache'; 10 | import { IAuthOptions } from '../IAuthOptions'; 11 | 12 | export class FileConfig implements IAuthResolver { 13 | private static CredsCache: Cache = new Cache(); 14 | 15 | constructor(private _siteUrl: string) { } 16 | 17 | public getAuth(): Promise { 18 | const fileNameTemplate = FilesHelper.resolveFileName(this._siteUrl); 19 | 20 | const cachedCreds = FileConfig.CredsCache.get(fileNameTemplate); 21 | 22 | if (cachedCreds) { 23 | return AuthResolverFactory.resolve(this._siteUrl, cachedCreds).getAuth(); 24 | } 25 | 26 | const userDataFolder = FilesHelper.getUserDataFolder(); 27 | const credsFolder = path.join(userDataFolder, 'creds'); 28 | 29 | if (!fs.existsSync(credsFolder)) { 30 | fs.mkdirSync(credsFolder); 31 | } 32 | 33 | const fileNames = fs.readdirSync(credsFolder).map(name => { 34 | return path.basename(name, path.extname(name)); 35 | }); 36 | 37 | let configPath = this.findBestMatch(fileNameTemplate, fileNames); 38 | 39 | if (!configPath) { 40 | configPath = path.join(credsFolder, `${fileNameTemplate}.json`); 41 | } else { 42 | configPath = path.join(credsFolder, `${configPath}.json`); 43 | } 44 | 45 | const config = new AuthConfig({ 46 | configPath: configPath, 47 | encryptPassword: true, 48 | saveConfigOnDisk: true 49 | }); 50 | 51 | return Promise.resolve(config.getContext()) 52 | .then(context => { 53 | const fileNameTemplate = FilesHelper.resolveFileName(context.siteUrl); 54 | const fileNameWithoutExt = path.basename(configPath, path.extname(configPath)); 55 | 56 | if (fileNameWithoutExt !== fileNameTemplate) { 57 | const fileName = path.basename(configPath); 58 | const newPath = configPath.replace(fileName, `${fileNameTemplate}.json`); 59 | fs.renameSync(configPath, newPath); 60 | } 61 | 62 | return context.authOptions; 63 | }) 64 | .then(authOptions => { 65 | FileConfig.CredsCache.set(fileNameTemplate, authOptions); 66 | return AuthResolverFactory.resolve(this._siteUrl, authOptions).getAuth(); 67 | }); 68 | } 69 | 70 | private findBestMatch(fileNameTemplate: string, fileNames: string[]): string { 71 | let matchLength = 2048; 72 | let matchFileName: string = null; 73 | 74 | fileNames.forEach(fileName => { 75 | if (fileNameTemplate.indexOf(fileName) !== -1) { 76 | const subUrlLength = fileNameTemplate.replace(fileName, '').length; 77 | if (subUrlLength < matchLength) { 78 | matchLength = subUrlLength; 79 | matchFileName = fileName; 80 | } 81 | } 82 | }); 83 | 84 | return matchFileName; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/auth/resolvers/OnDemand/OnDemand.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'child_process'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import { Cpass } from 'cpass'; 5 | 6 | import { IAuthResolver } from '../../IAuthResolver'; 7 | import { IAuthResponse } from '../../IAuthResponse'; 8 | import { IOnDemandCredentials, isOnPremUrl } from '../../IAuthOptions'; 9 | import { Cache } from './../../../utils/Cache'; 10 | import { FilesHelper } from '../../../utils/FilesHelper'; 11 | 12 | export interface ICookie { 13 | httpOnly: boolean; 14 | name: string; 15 | value: string; 16 | expirationDate?: number; 17 | } 18 | 19 | export class OnDemand implements IAuthResolver { 20 | private static CookieCache: Cache = new Cache(); 21 | private _cpass = new Cpass(); 22 | 23 | constructor(private _siteUrl: string, private _authOptions: IOnDemandCredentials) { 24 | // probably we are trying to get auth data using url http://site/_api/web/etc 25 | // which will never work for on-demand option, so strip the url to just http://site 26 | // that a case for spsave or sp-request 27 | if (this._siteUrl.indexOf('/_') !== -1) { 28 | const indx = this._siteUrl.indexOf('/_'); 29 | this._siteUrl = this._siteUrl.substr(0, indx); 30 | } 31 | 32 | this._authOptions = Object.assign({ 33 | force: false, 34 | persist: true 35 | }, this._authOptions); 36 | } 37 | 38 | public getAuth(): Promise { 39 | const dataFilePath = this.getDataFilePath(); 40 | let cookies: ICookie[]; 41 | const cacheKey: string = FilesHelper.resolveFileName(this._siteUrl); 42 | 43 | const cachedCookie: string = OnDemand.CookieCache.get(cacheKey); 44 | 45 | if (cachedCookie) { 46 | return Promise.resolve({ 47 | headers: { 48 | 'Cookie': cachedCookie 49 | } 50 | }); 51 | } 52 | 53 | if (!fs.existsSync(dataFilePath) || this._authOptions.force) { 54 | cookies = this.saveAuthData(dataFilePath); 55 | } else { 56 | console.log(`[node-sp-auth]: reading auth data from ${dataFilePath}`); 57 | 58 | cookies = JSON.parse(this._cpass.decode(fs.readFileSync(dataFilePath).toString())); 59 | let expired = false; 60 | cookies.forEach((cookie) => { 61 | const now = new Date(); 62 | if (cookie.expirationDate && new Date(cookie.expirationDate * 1000) < now) { 63 | expired = true; 64 | } 65 | }); 66 | 67 | if (expired) { 68 | cookies = this.saveAuthData(dataFilePath); 69 | } 70 | } 71 | 72 | let authCookie = ''; 73 | 74 | cookies.forEach((cookie) => { 75 | authCookie += `${cookie.name}=${cookie.value};`; 76 | }); 77 | 78 | authCookie = authCookie.slice(0, -1); 79 | OnDemand.CookieCache.set(cacheKey, authCookie, this.getMaxExpiration(cookies)); 80 | 81 | return Promise.resolve({ 82 | headers: { 83 | 'Cookie': authCookie 84 | } 85 | }); 86 | } 87 | 88 | private getMaxExpiration(cookies: ICookie[]): Date { 89 | let expiration = 0; 90 | cookies.forEach(cookie => { 91 | if (cookie.expirationDate > expiration) { 92 | expiration = cookie.expirationDate * 1000; 93 | } 94 | }); 95 | 96 | return new Date(expiration); 97 | } 98 | 99 | private saveAuthData(dataPath: string): ICookie[] { 100 | 101 | const electronExecutable = this._authOptions.electron || 'electron'; 102 | const electronProc = childProcess.spawnSync( 103 | electronExecutable, 104 | [ 105 | path.join(__dirname, 'electron/main.js'), 106 | '--', 107 | this._siteUrl, 108 | this._authOptions.force === true ? 'true' : 'false' 109 | ] 110 | ); 111 | const output = electronProc.stdout.toString(); 112 | 113 | const cookieRegex = /#\{([\s\S]+?)\}#/gm; 114 | const cookieData = cookieRegex.exec(output); 115 | 116 | const cookiesJson = cookieData[1].split(';#;'); 117 | const cookies: ICookie[] = []; 118 | 119 | cookiesJson.forEach((cookie) => { 120 | const data: string = cookie.replace(/(\n|\r)+/g, '').replace(/^["]+|["]+$/g, ''); 121 | if (data) { 122 | const cookieData = JSON.parse(data) as ICookie; 123 | if (cookieData.httpOnly) { 124 | cookies.push(cookieData); 125 | 126 | // explicitly set 1 hour expiration for on-premise 127 | if (isOnPremUrl(this._siteUrl)) { 128 | const expiration = new Date(); 129 | expiration.setMinutes(expiration.getMinutes() + (this._authOptions.ttl || 55)); 130 | cookieData.expirationDate = expiration.getTime() / 1000; 131 | } else if (!cookieData.expirationDate) { // 24 hours for online if no expiration date on cookie 132 | const expiration = new Date(); 133 | expiration.setMinutes(expiration.getMinutes() + (this._authOptions.ttl || 1435)); 134 | cookieData.expirationDate = expiration.getTime() / 1000; 135 | } 136 | } 137 | } 138 | }); 139 | 140 | if (cookies.length === 0) { 141 | throw new Error('Cookie array is empty'); 142 | } 143 | 144 | if (this._authOptions.persist) { 145 | fs.writeFileSync(dataPath, this._cpass.encode(JSON.stringify(cookies))); 146 | } 147 | 148 | return cookies; 149 | } 150 | 151 | private getDataFilePath(): string { 152 | const userDataFolder = FilesHelper.getUserDataFolder(); 153 | const ondemandFolder = path.join(userDataFolder, 'ondemand'); 154 | 155 | if (!fs.existsSync(ondemandFolder)) { 156 | fs.mkdirSync(ondemandFolder); 157 | } 158 | return path.join(ondemandFolder, `${FilesHelper.resolveFileName(this._siteUrl)}.data`); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/auth/resolvers/OnlineAddinOnly.ts: -------------------------------------------------------------------------------- 1 | import { request } from './../../config'; 2 | import * as url from 'url'; 3 | 4 | import { IOnlineAddinCredentials } from './../IAuthOptions'; 5 | import { IAuthResponse } from './../IAuthResponse'; 6 | import { Cache } from './../../utils/Cache'; 7 | import { UrlHelper } from './../../utils/UrlHelper'; 8 | import * as consts from './../../Consts'; 9 | import { OnlineResolver } from './base/OnlineResolver'; 10 | import { HostingEnvironment } from '../HostingEnvironment'; 11 | 12 | export class OnlineAddinOnly extends OnlineResolver { 13 | 14 | private static TokenCache: Cache = new Cache(); 15 | 16 | constructor(_siteUrl: string, private _authOptions: IOnlineAddinCredentials) { 17 | super(_siteUrl); 18 | } 19 | 20 | public getAuth(): Promise { 21 | const sharepointhostname: string = url.parse(this._siteUrl).hostname; 22 | const cacheKey = `${sharepointhostname}@${this._authOptions.clientSecret}@${this._authOptions.clientId}`; 23 | 24 | const cachedToken: string = OnlineAddinOnly.TokenCache.get(cacheKey); 25 | 26 | if (cachedToken) { 27 | return Promise.resolve({ 28 | headers: { 29 | 'Authorization': `Bearer ${cachedToken}` 30 | } 31 | }); 32 | } 33 | 34 | return this.getRealm(this._siteUrl) 35 | .then(realm => { 36 | return Promise.all([realm, this.getAuthUrl(realm)]); 37 | }) 38 | .then(data => { 39 | const realm: string = data[0]; 40 | const authUrl: string = data[1]; 41 | 42 | const resource = `${consts.SharePointServicePrincipal}/${sharepointhostname}@${realm}`; 43 | const fullClientId = `${this._authOptions.clientId}@${realm}`; 44 | 45 | return request.post(authUrl, { 46 | form: { 47 | 'grant_type': 'client_credentials', 48 | 'client_id': fullClientId, 49 | 'client_secret': this._authOptions.clientSecret, 50 | 'resource': resource 51 | } 52 | }).json<{ expires_in: string, access_token: string }>(); 53 | }) 54 | .then(data => { 55 | const expiration: number = parseInt(data.expires_in, 10); 56 | OnlineAddinOnly.TokenCache.set(cacheKey, data.access_token, expiration - 60); 57 | 58 | return { 59 | headers: { 60 | 'Authorization': `Bearer ${data.access_token}` 61 | } 62 | }; 63 | }); 64 | } 65 | 66 | protected InitEndpointsMappings(): void { 67 | this.endpointsMappings.set(HostingEnvironment.Production, 'accounts.accesscontrol.windows.net'); 68 | this.endpointsMappings.set(HostingEnvironment.China, 'accounts.accesscontrol.chinacloudapi.cn'); 69 | this.endpointsMappings.set(HostingEnvironment.German, 'login.microsoftonline.de'); 70 | this.endpointsMappings.set(HostingEnvironment.USDefence, 'accounts.accesscontrol.windows.net'); 71 | this.endpointsMappings.set(HostingEnvironment.USGovernment, 'accounts.accesscontrol.windows.net'); 72 | } 73 | 74 | private getAuthUrl(realm: string): Promise { 75 | return new Promise((resolve, reject) => { 76 | const url = this.AcsRealmUrl + realm; 77 | 78 | request.get(url).json() 79 | .then((data: { endpoints: { protocol: string, location: string }[] }) => { 80 | for (let i = 0; i < data.endpoints.length; i++) { 81 | if (data.endpoints[i].protocol === 'OAuth2') { 82 | resolve(data.endpoints[i].location); 83 | return undefined; 84 | } 85 | } 86 | }) 87 | .catch(reject); 88 | }); 89 | } 90 | 91 | private get AcsRealmUrl(): string { 92 | return `https://${this.endpointsMappings.get(this.hostingEnvironment)}/metadata/json/1?realm=`; 93 | } 94 | 95 | private getRealm(siteUrl: string): Promise { 96 | 97 | if (this._authOptions.realm) { 98 | return Promise.resolve(this._authOptions.realm); 99 | } 100 | 101 | return request.post(`${UrlHelper.removeTrailingSlash(siteUrl)}/_vti_bin/client.svc`, { 102 | method: 'POST', 103 | headers: { 104 | 'Authorization': 'Bearer ' 105 | } 106 | }) 107 | .then(data => { 108 | const header: string = data.headers['www-authenticate']; 109 | const index: number = header.indexOf('Bearer realm="'); 110 | return header.substring(index + 14, index + 50); 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/auth/resolvers/OnlineUserCredentials.ts: -------------------------------------------------------------------------------- 1 | import * as url from 'url'; 2 | import { request } from './../../config'; 3 | import * as cookie from 'cookie'; 4 | import template = require('lodash.template'); 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const xmldoc: any = require('xmldoc'); 8 | 9 | import { IUserCredentials } from './../IAuthOptions'; 10 | import { IAuthResponse } from './../IAuthResponse'; 11 | import { Cache } from './../../utils/Cache'; 12 | import * as consts from './../../Consts'; 13 | import { AdfsHelper } from './../../utils/AdfsHelper'; 14 | 15 | import { template as onlineSamlWsfedAdfsTemplate } from './../../templates/OnlineSamlWsfedAdfs'; 16 | import { template as onlineSamlWsfedTemplate } from './../../templates/OnlineSamlWsfed'; 17 | import { HostingEnvironment } from '../HostingEnvironment'; 18 | import { OnlineResolver } from './base/OnlineResolver'; 19 | import { Response } from 'got'; 20 | 21 | export class OnlineUserCredentials extends OnlineResolver { 22 | 23 | private static CookieCache: Cache = new Cache(); 24 | 25 | constructor(_siteUrl: string, private _authOptions: IUserCredentials) { 26 | super(_siteUrl); 27 | 28 | this._authOptions = Object.assign({}, _authOptions); 29 | 30 | this._authOptions.username = this._authOptions.username 31 | .replace(/&/g, '&') 32 | .replace(/"/g, '"') 33 | .replace(/'/g, ''') 34 | .replace(//g, '>'); 36 | 37 | this._authOptions.password = this._authOptions.password 38 | .replace(/&/g, '&') 39 | .replace(/"/g, '"') 40 | .replace(/'/g, ''') 41 | .replace(//g, '>'); 43 | } 44 | 45 | public getAuth(): Promise { 46 | const parsedUrl: url.Url = url.parse(this._siteUrl); 47 | const host: string = parsedUrl.host; 48 | const cacheKey = `${host}@${this._authOptions.username}@${this._authOptions.password}`; 49 | const cachedCookie: string = OnlineUserCredentials.CookieCache.get(cacheKey); 50 | 51 | if (cachedCookie) { 52 | return Promise.resolve({ 53 | headers: { 54 | 'Cookie': cachedCookie 55 | } 56 | }); 57 | } 58 | 59 | return this.getSecurityToken() 60 | .then(xmlResponse => { 61 | return this.postToken(xmlResponse); 62 | }) 63 | .then(data => { 64 | const response = data[1]; 65 | const diffSeconds: number = data[0]; 66 | let fedAuth: string; 67 | let rtFa: string; 68 | 69 | for (let i = 0; i < response.headers['set-cookie'].length; i++) { 70 | const headerCookie: string = response.headers['set-cookie'][i]; 71 | if (headerCookie.indexOf(consts.FedAuth) !== -1) { 72 | fedAuth = cookie.parse(headerCookie)[consts.FedAuth]; 73 | } 74 | if (headerCookie.indexOf(consts.RtFa) !== -1) { 75 | rtFa = cookie.parse(headerCookie)[consts.RtFa]; 76 | } 77 | } 78 | 79 | const authCookie: string = 'FedAuth=' + fedAuth + '; rtFa=' + rtFa; 80 | 81 | OnlineUserCredentials.CookieCache.set(cacheKey, authCookie, diffSeconds); 82 | 83 | return { 84 | headers: { 85 | 'Cookie': authCookie 86 | } 87 | }; 88 | }); 89 | } 90 | 91 | protected InitEndpointsMappings(): void { 92 | this.endpointsMappings.set(HostingEnvironment.Production, 'login.microsoftonline.com'); 93 | this.endpointsMappings.set(HostingEnvironment.China, 'login.chinacloudapi.cn'); 94 | this.endpointsMappings.set(HostingEnvironment.German, 'login.microsoftonline.de'); 95 | this.endpointsMappings.set(HostingEnvironment.USDefence, 'login-us.microsoftonline.com'); 96 | this.endpointsMappings.set(HostingEnvironment.USGovernment, 'login.microsoftonline.us'); 97 | } 98 | 99 | private getSecurityToken(): Promise { 100 | return request.post(this.OnlineUserRealmEndpoint, { 101 | form: { 102 | 'login': this._authOptions.username 103 | } 104 | }).json() 105 | .then((userRealm: any) => { 106 | const authType: string = userRealm.NameSpaceType; 107 | 108 | if (!authType) { 109 | throw new Error('Unable to define namespace type for Online authentiation'); 110 | } 111 | 112 | if (authType === 'Managed') { 113 | return this.getSecurityTokenWithOnline(); 114 | } 115 | 116 | if (authType === 'Federated') { 117 | return this.getSecurityTokenWithAdfs(userRealm.AuthURL, userRealm.CloudInstanceIssuerUri); 118 | } 119 | 120 | throw new Error(`Unable to resolve namespace authentiation type. Type received: ${authType}`); 121 | }); 122 | } 123 | 124 | private getSecurityTokenWithAdfs(adfsUrl: string, relyingParty: string): Promise { 125 | return AdfsHelper.getSamlAssertion({ 126 | username: this._authOptions.username, 127 | password: this._authOptions.password, 128 | adfsUrl: adfsUrl, 129 | relyingParty: relyingParty || consts.AdfsOnlineRealm 130 | }) 131 | .then(samlAssertion => { 132 | 133 | const siteUrlParsed: url.Url = url.parse(this._siteUrl); 134 | const rootSiteUrl: string = siteUrlParsed.protocol + '//' + siteUrlParsed.host; 135 | const tokenRequest: string = template(onlineSamlWsfedAdfsTemplate)({ 136 | endpoint: rootSiteUrl, 137 | token: samlAssertion.value 138 | }); 139 | 140 | return request.post(this.MSOnlineSts, { 141 | body: tokenRequest, 142 | headers: { 143 | 'Content-Length': tokenRequest.length.toString(), 144 | 'Content-Type': 'application/soap+xml; charset=utf-8' 145 | }, 146 | resolveBodyOnly: true 147 | }); 148 | }); 149 | } 150 | 151 | private getSecurityTokenWithOnline(): Promise { 152 | const parsedUrl: url.Url = url.parse(this._siteUrl); 153 | const host: string = parsedUrl.host; 154 | const spFormsEndPoint = `${parsedUrl.protocol}//${host}/${consts.FormsPath}`; 155 | 156 | const samlBody: string = template(onlineSamlWsfedTemplate)({ 157 | username: this._authOptions.username, 158 | password: this._authOptions.password, 159 | endpoint: spFormsEndPoint 160 | }); 161 | 162 | return request 163 | .post(this.MSOnlineSts, { 164 | body: samlBody, 165 | resolveBodyOnly: true, 166 | headers: { 167 | 'Content-Type': 'application/soap+xml; charset=utf-8' 168 | } 169 | }); 170 | } 171 | 172 | private postToken(xmlResponse: any): Promise<[number, Response]> { 173 | const xmlDoc: any = new xmldoc.XmlDocument(xmlResponse); 174 | const parsedUrl: url.Url = url.parse(this._siteUrl); 175 | const spFormsEndPoint = `${parsedUrl.protocol}//${parsedUrl.host}/${consts.FormsPath}`; 176 | 177 | const securityTokenResponse: any = xmlDoc.childNamed('S:Body').firstChild; 178 | if (securityTokenResponse.name.indexOf('Fault') !== -1) { 179 | throw new Error(securityTokenResponse.toString()); 180 | } 181 | 182 | const binaryToken: any = securityTokenResponse.childNamed('wst:RequestedSecurityToken').firstChild.val; 183 | const now: any = new Date().getTime(); 184 | const expires: number = new Date(securityTokenResponse.childNamed('wst:Lifetime').childNamed('wsu:Expires').val).getTime(); 185 | const diff: number = (expires - now) / 1000; 186 | 187 | const diffSeconds: number = parseInt(diff.toString(), 10); 188 | 189 | return Promise.all([Promise.resolve(diffSeconds), request 190 | .post(spFormsEndPoint, { 191 | headers: { 192 | 'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)', 193 | 'Content-Type': 'application/x-www-form-urlencoded' 194 | }, 195 | body: binaryToken 196 | })]); 197 | } 198 | 199 | private get MSOnlineSts(): string { 200 | return `https://${this.endpointsMappings.get(this.hostingEnvironment)}/extSTS.srf`; 201 | } 202 | 203 | private get OnlineUserRealmEndpoint(): string { 204 | return `https://${this.endpointsMappings.get(this.hostingEnvironment)}/GetUserRealm.srf`; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/auth/resolvers/OnpremiseAddinOnly.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import * as fs from 'fs'; 3 | import * as url from 'url'; 4 | 5 | import { IAuthResolver } from './../IAuthResolver'; 6 | import { IOnPremiseAddinCredentials } from './../IAuthOptions'; 7 | import { IAuthResponse } from './../IAuthResponse'; 8 | import { Cache } from './../../utils/Cache'; 9 | import * as consts from './../../Consts'; 10 | 11 | export class OnpremiseAddinOnly implements IAuthResolver { 12 | private static TokenCache: Cache = new Cache(); 13 | 14 | constructor(private _siteUrl: string, private _authOptions: IOnPremiseAddinCredentials) { } 15 | 16 | public getAuth(): Promise { 17 | 18 | const sharepointhostname: string = url.parse(this._siteUrl).host; 19 | const audience = `${consts.SharePointServicePrincipal}/${sharepointhostname}@${this._authOptions.realm}`; 20 | const fullIssuerIdentifier = `${this._authOptions.issuerId}@${this._authOptions.realm}`; 21 | 22 | const options: any = { 23 | key: fs.readFileSync(this._authOptions.rsaPrivateKeyPath) 24 | }; 25 | 26 | const dateref: number = parseInt(((new Date()).getTime() / 1000).toString(), 10); 27 | 28 | const rs256: any = { 29 | typ: 'JWT', 30 | alg: 'RS256', 31 | x5t: this._authOptions.shaThumbprint 32 | }; 33 | 34 | const actortoken: any = { 35 | aud: audience, 36 | iss: fullIssuerIdentifier, 37 | nameid: this._authOptions.clientId + '@' + this._authOptions.realm, 38 | nbf: dateref - consts.HighTrustTokenLifeTime, 39 | exp: dateref + consts.HighTrustTokenLifeTime, 40 | trustedfordelegation: true 41 | }; 42 | 43 | const cacheKey: string = audience; 44 | const cachedToken: string = OnpremiseAddinOnly.TokenCache.get(cacheKey); 45 | let accessToken: string; 46 | 47 | if (cachedToken) { 48 | accessToken = cachedToken; 49 | } else { 50 | accessToken = jwt.sign(actortoken, options.key, { header: rs256 }); 51 | OnpremiseAddinOnly.TokenCache.set(cacheKey, accessToken, consts.HighTrustTokenLifeTime - 60); 52 | } 53 | 54 | return Promise.resolve({ 55 | headers: { 56 | 'Authorization': `Bearer ${accessToken}` 57 | } 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/auth/resolvers/OnpremiseFbaCredentials.ts: -------------------------------------------------------------------------------- 1 | import * as url from 'url'; 2 | import { request } from './../../config'; 3 | import * as cookie from 'cookie'; 4 | import template = require('lodash.template'); 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const xmldoc: any = require('xmldoc'); 8 | 9 | import { IAuthResolver } from './../IAuthResolver'; 10 | import { IOnpremiseUserCredentials } from './../IAuthOptions'; 11 | import { IAuthResponse } from './../IAuthResponse'; 12 | import { Cache } from './../../utils/Cache'; 13 | import * as consts from './../../Consts'; 14 | 15 | import { template as fbaLoginWsfedTemplate } from './../../templates/FbaLoginWsfed'; 16 | 17 | export class OnpremiseFbaCredentials implements IAuthResolver { 18 | 19 | private static CookieCache: Cache = new Cache(); 20 | 21 | constructor(private _siteUrl: string, private _authOptions: IOnpremiseUserCredentials) { } 22 | 23 | public getAuth(): Promise { 24 | 25 | const parsedUrl: url.Url = url.parse(this._siteUrl); 26 | const host: string = parsedUrl.host; 27 | const cacheKey = `${host}@${this._authOptions.username}@${this._authOptions.password}`; 28 | const cachedCookie: string = OnpremiseFbaCredentials.CookieCache.get(cacheKey); 29 | 30 | if (cachedCookie) { 31 | return Promise.resolve({ 32 | headers: { 33 | 'Cookie': cachedCookie 34 | } 35 | }); 36 | } 37 | 38 | const soapBody: string = template(fbaLoginWsfedTemplate)({ 39 | username: this._authOptions.username, 40 | password: this._authOptions.password 41 | }); 42 | 43 | const fbaEndPoint = `${parsedUrl.protocol}//${host}/${consts.FbaAuthEndpoint}`; 44 | 45 | return request({ 46 | url: fbaEndPoint, 47 | method: 'POST', 48 | headers: { 49 | 'Content-Type': 'text/xml; charset=utf-8', 50 | 'Content-Length': soapBody.length.toString() 51 | }, 52 | body: soapBody 53 | }) 54 | .then(response => { 55 | 56 | const xmlDoc: any = new xmldoc.XmlDocument(response.body); 57 | 58 | if (xmlDoc.name === 'm:error') { 59 | const errorCode: string = xmlDoc.childNamed('m:code').val; 60 | const errorMessage: string = xmlDoc.childNamed('m:message').val; 61 | throw new Error(`${errorCode}, ${errorMessage}`); 62 | } 63 | 64 | const errorCode: string = 65 | xmlDoc.childNamed('soap:Body').childNamed('LoginResponse').childNamed('LoginResult').childNamed('ErrorCode').val; 66 | const cookieName: string = 67 | xmlDoc.childNamed('soap:Body').childNamed('LoginResponse').childNamed('LoginResult').childNamed('CookieName').val; 68 | const diffSeconds: number = parseInt( 69 | xmlDoc.childNamed('soap:Body').childNamed('LoginResponse').childNamed('LoginResult').childNamed('TimeoutSeconds').val, 70 | null 71 | ); 72 | let cookieValue: string; 73 | 74 | if (errorCode === 'PasswordNotMatch') { 75 | throw new Error('Password doesn\'t not match'); 76 | } 77 | if (errorCode !== 'NoError') { 78 | throw new Error(errorCode); 79 | } 80 | 81 | (response.headers['set-cookie'] || []).forEach((headerCookie: string) => { 82 | if (headerCookie.indexOf(cookieName) !== -1) { 83 | cookieValue = cookie.parse(headerCookie)[cookieName]; 84 | } 85 | }); 86 | 87 | const authCookie = `${cookieName}=${cookieValue}`; 88 | 89 | OnpremiseFbaCredentials.CookieCache.set(cacheKey, authCookie, diffSeconds); 90 | 91 | return { 92 | headers: { 93 | 'Cookie': authCookie 94 | } 95 | }; 96 | 97 | }) as Promise; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/auth/resolvers/OnpremiseTmgCredentials.ts: -------------------------------------------------------------------------------- 1 | import * as url from 'url'; 2 | import { request } from './../../config'; 3 | import * as http from 'http'; 4 | import * as https from 'https'; 5 | 6 | import { IAuthResolver } from './../IAuthResolver'; 7 | import { IOnpremiseUserCredentials } from './../IAuthOptions'; 8 | import { IAuthResponse } from './../IAuthResponse'; 9 | import { Cache } from './../../utils/Cache'; 10 | import * as consts from './../../Consts'; 11 | 12 | export class OnpremiseTmgCredentials implements IAuthResolver { 13 | 14 | private static CookieCache: Cache = new Cache(); 15 | 16 | constructor(private _siteUrl: string, private _authOptions: IOnpremiseUserCredentials) { } 17 | 18 | public getAuth(): Promise { 19 | 20 | const parsedUrl: url.Url = url.parse(this._siteUrl); 21 | const host: string = parsedUrl.host; 22 | const cacheKey = `${host}@${this._authOptions.username}@${this._authOptions.password}`; 23 | const cachedCookie: string = OnpremiseTmgCredentials.CookieCache.get(cacheKey); 24 | 25 | if (cachedCookie) { 26 | return Promise.resolve({ 27 | headers: { 28 | 'Cookie': cachedCookie 29 | } 30 | }); 31 | } 32 | 33 | const tmgEndPoint = `${parsedUrl.protocol}//${host}/${consts.TmgAuthEndpoint}`; 34 | 35 | const isHttps: boolean = url.parse(this._siteUrl).protocol === 'https:'; 36 | 37 | const keepaliveAgent: any = isHttps ? 38 | new https.Agent({ keepAlive: true, rejectUnauthorized: !!this._authOptions.rejectUnauthorized }) : 39 | new http.Agent({ keepAlive: true }); 40 | 41 | return request({ 42 | url: tmgEndPoint, 43 | method: 'POST', 44 | headers: { 45 | 'Content-Type': 'application/x-www-form-urlencoded' 46 | }, 47 | body: 'curl=Z2F&flags=0&forcedownlevel=0&formdir=1&trusted=0&' + 48 | `username=${encodeURIComponent(this._authOptions.username)}&` + 49 | `password=${encodeURIComponent(this._authOptions.password)}`, 50 | agent: keepaliveAgent 51 | }) 52 | .then(response => { 53 | 54 | const authCookie = response.headers['set-cookie'][0]; 55 | 56 | return { 57 | headers: { 58 | 'Cookie': authCookie 59 | } 60 | }; 61 | 62 | }) as Promise; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/auth/resolvers/OnpremiseUserCredentials.ts: -------------------------------------------------------------------------------- 1 | import * as url from 'url'; 2 | import { request } from './../../config'; 3 | import * as http from 'http'; 4 | import * as https from 'https'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const ntlm: any = require('node-ntlm-client/lib/ntlm'); 8 | 9 | import { IAuthResolver } from './../IAuthResolver'; 10 | import { IOnpremiseUserCredentials } from './../IAuthOptions'; 11 | import { IAuthResponse } from './../IAuthResponse'; 12 | 13 | export class OnpremiseUserCredentials implements IAuthResolver { 14 | 15 | constructor(private _siteUrl: string, private _authOptions: IOnpremiseUserCredentials) { } 16 | 17 | public getAuth(): Promise { 18 | 19 | const ntlmOptions: any = Object.assign({}, this._authOptions); 20 | ntlmOptions.url = this._siteUrl; 21 | 22 | if (ntlmOptions.username.indexOf('\\') > 0) { 23 | const parts = ntlmOptions.username.split('\\'); 24 | ntlmOptions.username = parts[1]; 25 | ntlmOptions.domain = parts[0].toUpperCase(); 26 | } 27 | 28 | // check upn case, i.e. user@domain.com 29 | if (ntlmOptions.username.indexOf('@') > 0) { 30 | ntlmOptions.domain = ''; 31 | } 32 | 33 | const type1msg: any = ntlm.createType1Message(); 34 | 35 | const isHttps: boolean = url.parse(this._siteUrl).protocol === 'https:'; 36 | 37 | const keepaliveAgent: any = isHttps ? new https.Agent({ keepAlive: true, rejectUnauthorized: !!ntlmOptions.rejectUnauthorized }) : 38 | new http.Agent({ keepAlive: true }); 39 | 40 | return request({ 41 | url: this._siteUrl, 42 | method: 'GET', 43 | headers: { 44 | 'Connection': 'keep-alive', 45 | 'Authorization': type1msg, 46 | 'Accept': 'application/json;odata=verbose' 47 | }, 48 | agent: keepaliveAgent 49 | }) 50 | .then(response => { 51 | const type2msg: any = ntlm.decodeType2Message(response.headers['www-authenticate']); 52 | const type3msg: any = ntlm.createType3Message(type2msg, ntlmOptions.username, ntlmOptions.password, ntlmOptions.workstation, ntlmOptions.domain); 53 | 54 | return { 55 | headers: { 56 | 'Connection': 'Close', 57 | 'Authorization': type3msg 58 | }, 59 | options: { 60 | agent: keepaliveAgent 61 | } 62 | }; 63 | }) as Promise; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/auth/resolvers/base/OnlineResolver.ts: -------------------------------------------------------------------------------- 1 | import { IAuthResolver } from '../../IAuthResolver'; 2 | import { IAuthResponse } from '../../IAuthResponse'; 3 | import { HostingEnvironment } from '../../HostingEnvironment'; 4 | import { UrlHelper } from '../../../utils/UrlHelper'; 5 | 6 | export abstract class OnlineResolver implements IAuthResolver { 7 | 8 | protected hostingEnvironment: HostingEnvironment; 9 | protected endpointsMappings: Map; 10 | 11 | constructor(protected _siteUrl: string) { 12 | this.endpointsMappings = new Map(); 13 | this.hostingEnvironment = UrlHelper.ResolveHostingEnvironment(this._siteUrl); 14 | this.InitEndpointsMappings(); 15 | } 16 | 17 | public abstract getAuth(): Promise; 18 | protected abstract InitEndpointsMappings(): void; 19 | } 20 | -------------------------------------------------------------------------------- /src/auth/resolvers/ondemand/electron/main.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | const process = require('process'); 3 | 4 | // Module to control application life. 5 | const app = electron.app; 6 | app.commandLine.appendSwitch('ignore-certificate-errors'); 7 | app.commandLine.appendSwitch('no-sandbox'); 8 | // Module to create native browser window. 9 | const BrowserWindow = electron.BrowserWindow; 10 | 11 | const path = require('path'); 12 | const url = require('url'); 13 | 14 | // Keep a global reference of the window object, if you don't, the window will 15 | // be closed automatically when the JavaScript object is garbage collected. 16 | let mainWindow = null; 17 | 18 | const createWindow = () => { 19 | let siteUrl = process.argv[3]; 20 | let force = process.argv[4] === 'true'; 21 | if (siteUrl.endsWith('/')) { 22 | siteUrl = siteUrl.slice(0, -1); 23 | } 24 | 25 | // Create the browser window. 26 | mainWindow = new BrowserWindow({ 27 | width: 500, 28 | height: 650, 29 | title: `Opening ${siteUrl}`, 30 | center: true, 31 | webPreferences: { 32 | webSecurity: false 33 | } 34 | }); 35 | 36 | mainWindow.setMenu(null); 37 | // and load the index.html of the app. 38 | if (force) { 39 | (async () => { 40 | await mainWindow.webContents.session.clearStorageData(); 41 | mainWindow.loadURL(siteUrl); 42 | })().catch(error => { 43 | console.log(error); 44 | throw error; 45 | }); 46 | } else { 47 | mainWindow.loadURL(siteUrl); 48 | } 49 | 50 | mainWindow.webContents.on('dom-ready', (data) => { 51 | const loadedUrl = mainWindow.webContents.getURL(); 52 | if (loadedUrl.indexOf(siteUrl) !== -1 && (loadedUrl.indexOf(siteUrl + '/_layouts/15/start.aspx') !== -1 || loadedUrl.indexOf(siteUrl + '/_') === -1)) { 53 | const session = mainWindow.webContents.session; 54 | const host = url.parse(siteUrl).hostname; 55 | const isOnPrem = host.indexOf('.sharepoint.com') === -1 56 | && host.indexOf('.sharepoint.cn') === -1 57 | && host.indexOf('.sharepoint.de') === -1 58 | && host.indexOf('.sharepoint-mil.us') === -1 59 | && host.indexOf('.sharepoint.us') === -1; 60 | let domain; 61 | if (isOnPrem) { 62 | domain = host; 63 | } else if (host.indexOf('.sharepoint.com') !== -1) { 64 | domain = '.sharepoint.com'; 65 | } else if (host.indexOf('.sharepoint.cn') !== -1) { 66 | domain = '.sharepoint.cn'; 67 | } else if (host.indexOf('.sharepoint.de') !== -1) { 68 | domain = '.sharepoint.de'; 69 | } else if (host.indexOf('.sharepoint-mil.us') !== -1) { 70 | domain = '.sharepoint-mil.us'; 71 | } else if (host.indexOf('.sharepoint.us') !== -1) { 72 | domain = '.sharepoint.us'; 73 | } else { 74 | throw new Error('Unable to resolve domain'); 75 | } 76 | session.cookies.get({ domain: domain }) 77 | .then((cookies) => { 78 | console.log('#{'); 79 | cookies.forEach(cookie => { 80 | console.log(JSON.stringify(cookie)); 81 | console.log(';#;'); 82 | }); 83 | console.log('}#'); 84 | mainWindow.close(); 85 | }) 86 | .catch((error) => { 87 | console.log(error); 88 | throw error; 89 | }); 90 | } 91 | }); 92 | 93 | // Emitted when the window is closed. 94 | mainWindow.on('closed', () => { 95 | // Dereference the window object, usually you would store windows 96 | // in an array if your app supports multi windows, this is the time 97 | // when you should delete the corresponding element. 98 | mainWindow = null; 99 | }); 100 | }; 101 | 102 | app.on('login', (event, webContents, request, authInfo, callback) => { 103 | event.preventDefault(); 104 | let child = new BrowserWindow({ 105 | parent: mainWindow, 106 | modal: true, 107 | show: true, 108 | height: 100, 109 | width: 500 110 | }); 111 | child.setMenu(null); 112 | child.loadURL(`file://${__dirname}/no-ntlm.html`); 113 | child.on('closed', () => { 114 | mainWindow.close(); 115 | }); 116 | }); 117 | 118 | // This method will be called when Electron has finished 119 | // initialization and is ready to create browser windows. 120 | // Some APIs can only be used after this event occurs. 121 | app.on('ready', createWindow); 122 | 123 | // Quit when all windows are closed. 124 | app.on('window-all-closed', () => { 125 | app.quit(); 126 | }); 127 | 128 | app.on('activate', () => { 129 | // On OS X it's common to re-create a window in the app when the 130 | // dock icon is clicked and there are no other windows open. 131 | if (mainWindow === null) { 132 | createWindow(); 133 | } 134 | }); 135 | -------------------------------------------------------------------------------- /src/auth/resolvers/ondemand/electron/no-ntlm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Error 6 | 7 | 8 |

Integrated windows authentication isn't supported

9 | 10 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import got, { Options } from 'got'; 2 | import { 3 | bootstrap 4 | } from 'global-agent'; 5 | 6 | export interface IConfiguration { 7 | requestOptions?: Options; 8 | } 9 | 10 | if (process.env['http_proxy'] || process.env['https_proxy']) { 11 | if (process.env['http_proxy']) { 12 | process.env.GLOBAL_AGENT_HTTP_PROXY = process.env['http_proxy']; 13 | } 14 | if (process.env['https_proxy']) { 15 | process.env.GLOBAL_AGENT_HTTPS_PROXY = process.env['https_proxy']; 16 | } 17 | 18 | bootstrap(); 19 | } 20 | 21 | export let request: typeof got = got.extend({ followRedirect: false, rejectUnauthorized: false, throwHttpErrors: false, retry: 0 }); 22 | 23 | export function setup(config: IConfiguration): void { 24 | if (config.requestOptions) { 25 | request = request.extend(config.requestOptions); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { IAuthResponse } from './auth/IAuthResponse'; 2 | import { IAuthOptions } from './auth/IAuthOptions'; 3 | import { AuthResolverFactory } from './auth/AuthResolverFactory'; 4 | 5 | export function getAuth(url: string, options?: IAuthOptions): Promise { 6 | return AuthResolverFactory.resolve(url, options).getAuth(); 7 | } 8 | 9 | export * from './auth/IAuthOptions'; 10 | export * from './auth/IAuthResponse'; 11 | export * from './utils/TokenHelper'; 12 | export * from './auth/base'; 13 | export { setup, IConfiguration } from './config'; 14 | -------------------------------------------------------------------------------- /src/templates/AdfsSamlToken.ts: -------------------------------------------------------------------------------- 1 | export const template = ` 2 | 3 | 4 | <%= created %> 5 | <%= expires %> 6 | 7 | 8 | 9 | <%= relyingParty %> 10 | 11 | 12 | <%= token %> 13 | urn:oasis:names:tc:SAML:1.0:assertion 14 | http://schemas.xmlsoap.org/ws/2005/02/trust/Issue 15 | http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey 16 | 17 | `; 18 | -------------------------------------------------------------------------------- /src/templates/AdfsSamlWsfed.ts: -------------------------------------------------------------------------------- 1 | export const template = ` 2 | 3 | 4 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue 5 | 6 | http://www.w3.org/2005/08/addressing/anonymous 7 | 8 | <%= to %> 9 | 10 | 11 | <%= username %> 12 | <%= password %> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <%= relyingParty %> 21 | 22 | 23 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer 24 | http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue 25 | 26 | 27 | 28 | `; 29 | -------------------------------------------------------------------------------- /src/templates/FbaLoginWsfed.ts: -------------------------------------------------------------------------------- 1 | export const template = ` 2 | 3 | 4 | 5 | <%= username %> 6 | <%= password %> 7 | 8 | 9 | 10 | `; 11 | -------------------------------------------------------------------------------- /src/templates/OnlineSamlWsfed.ts: -------------------------------------------------------------------------------- 1 | export const template = ` 2 | 3 | 4 | http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue 5 | 6 | http://www.w3.org/2005/08/addressing/anonymous 7 | 8 | https://login.microsoftonline.com/extSTS.srf 9 | 10 | 11 | <%= username %> 12 | <%= password %> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <%= endpoint %> 21 | 22 | 23 | http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey 24 | http://schemas.xmlsoap.org/ws/2005/02/trust/Issue 25 | urn:oasis:names:tc:SAML:1.0:assertion 26 | 27 | 28 | 29 | `; 30 | -------------------------------------------------------------------------------- /src/templates/OnlineSamlWsfedAdfs.ts: -------------------------------------------------------------------------------- 1 | export const template = ` 2 | 3 | 4 | http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue 5 | 6 | http://www.w3.org/2005/08/addressing/anonymous 7 | 8 | https://login.microsoftonline.com/extSTS.srf 9 | <%= token %> 10 | 11 | 12 | 13 | 14 | 15 | <%= endpoint %> 16 | 17 | 18 | http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey 19 | http://schemas.xmlsoap.org/ws/2005/02/trust/Issue 20 | urn:oasis:names:tc:SAML:1.0:assertion 21 | 22 | 23 | 24 | `; 25 | -------------------------------------------------------------------------------- /src/utils/AdfsHelper.ts: -------------------------------------------------------------------------------- 1 | import { request } from './../config'; 2 | import * as url from 'url'; 3 | import template = require('lodash.template'); 4 | // eslint-disable-next-line @typescript-eslint/no-var-requires 5 | const xmldoc: any = require('xmldoc'); 6 | 7 | import { IAdfsUserCredentials } from './../auth/IAuthOptions'; 8 | import { SamlAssertion } from './SamlAssertion'; 9 | 10 | import { template as adfsSamlWsfedTemplate } from './../templates/AdfsSamlWsfed'; 11 | 12 | export class AdfsHelper { 13 | public static getSamlAssertion(credentials: IAdfsUserCredentials): Promise { 14 | const adfsHost: string = url.parse(credentials.adfsUrl).host; 15 | const usernameMixedUrl = `https://${adfsHost}/adfs/services/trust/13/usernamemixed`; 16 | 17 | const samlBody: string = template(adfsSamlWsfedTemplate)({ 18 | to: usernameMixedUrl, 19 | username: credentials.username, 20 | password: credentials.password, 21 | relyingParty: credentials.relyingParty 22 | }); 23 | 24 | return request.post(usernameMixedUrl, { 25 | body: samlBody, 26 | resolveBodyOnly: true, 27 | headers: { 28 | 'Content-Length': samlBody.length.toString(), 29 | 'Content-Type': 'application/soap+xml; charset=utf-8' 30 | } 31 | }) 32 | .then(xmlResponse => { 33 | const doc: any = new xmldoc.XmlDocument(xmlResponse); 34 | 35 | const tokenResponseCollection: any = doc.childNamed('s:Body').firstChild; 36 | if (tokenResponseCollection.name.indexOf('Fault') !== -1) { 37 | throw new Error(tokenResponseCollection.toString()); 38 | } 39 | 40 | const responseNamespace: string = tokenResponseCollection.name.split(':')[0]; 41 | const securityTokenResponse: any = doc.childNamed('s:Body').firstChild.firstChild; 42 | const samlAssertion: any = securityTokenResponse.childNamed(responseNamespace + ':RequestedSecurityToken').firstChild; 43 | const notBefore: string = samlAssertion.firstChild.attr['NotBefore']; 44 | const notAfter: string = samlAssertion.firstChild.attr['NotOnOrAfter']; 45 | 46 | return { 47 | value: samlAssertion.toString({ compressed: true }), 48 | notAfter: notAfter, 49 | notBefore: notBefore 50 | } as SamlAssertion; 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/Cache.ts: -------------------------------------------------------------------------------- 1 | import { CacheItem } from './CacheItem'; 2 | import * as crypto from 'crypto'; 3 | 4 | export class Cache { 5 | 6 | private _cache: { [key: string]: CacheItem } = {}; 7 | 8 | public set(key: string, data: T, expiration?: number | Date): void { 9 | let cacheItem: CacheItem = undefined; 10 | key = this.getHashKey(key); 11 | 12 | if (!expiration) { 13 | cacheItem = new CacheItem(data); 14 | } else if (typeof expiration === 'number') { 15 | const now: Date = new Date(); 16 | now.setSeconds(now.getSeconds() + expiration); 17 | cacheItem = new CacheItem(data, now); 18 | } else if (expiration instanceof Date) { 19 | cacheItem = new CacheItem(data, expiration); 20 | } 21 | 22 | this._cache[key] = cacheItem; 23 | } 24 | 25 | public get(key: string): T { 26 | key = this.getHashKey(key); 27 | const cacheItem: CacheItem = this._cache[key]; 28 | 29 | if (!cacheItem) { 30 | return undefined; 31 | } 32 | 33 | if (!cacheItem.expiredOn) { 34 | return cacheItem.data; 35 | } 36 | 37 | const now: Date = new Date(); 38 | 39 | if (now > cacheItem.expiredOn) { 40 | this.remove(key); 41 | return undefined; 42 | } else { 43 | return cacheItem.data; 44 | } 45 | } 46 | 47 | public remove(key: string): void { 48 | key = this.getHashKey(key); 49 | delete this._cache[key]; 50 | } 51 | 52 | public clear(): void { 53 | this._cache = {}; 54 | } 55 | 56 | private getHashKey(key: string): string { 57 | return crypto.createHash('md5').update(key).digest('hex'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/CacheItem.ts: -------------------------------------------------------------------------------- 1 | export class CacheItem { 2 | constructor(public data: any, public expiredOn?: Date) {} 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/FilesHelper.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | 4 | import { UrlHelper } from './UrlHelper'; 5 | 6 | export class FilesHelper { 7 | 8 | public static getUserDataFolder(): string { 9 | const platform = process.platform; 10 | let homepath: string; 11 | 12 | if (platform.lastIndexOf('win') === 0) { 13 | homepath = process.env.APPDATA || process.env.LOCALAPPDATA; 14 | } 15 | 16 | if (platform === 'darwin') { 17 | homepath = process.env.HOME; 18 | homepath = path.join(homepath, 'Library', 'Preferences'); 19 | } 20 | 21 | if (platform === 'linux') { 22 | homepath = process.env.HOME; 23 | } 24 | 25 | if (!homepath) { 26 | throw new Error('Couldn\'t find the base application data folder'); 27 | } 28 | 29 | const dataPath = path.join(homepath, 'spauth'); 30 | if (!fs.existsSync(dataPath)) { 31 | fs.mkdirSync(dataPath); 32 | } 33 | 34 | return dataPath; 35 | } 36 | 37 | public static resolveFileName(siteUrl: string): string { 38 | const url = FilesHelper.resolveSiteUrl(siteUrl); 39 | return url.replace(/[:/\s]/g, '_'); 40 | } 41 | 42 | private static resolveSiteUrl(siteUrl: string): string { 43 | if (siteUrl.indexOf('/_') === -1 && siteUrl.indexOf('/vti_') === -1) { 44 | return UrlHelper.removeTrailingSlash(siteUrl); 45 | } 46 | 47 | if (siteUrl.indexOf('/_') !== -1) { 48 | return siteUrl.slice(0, siteUrl.indexOf('/_')); 49 | } 50 | 51 | if (siteUrl.indexOf('/vti_') !== -1) { 52 | return siteUrl.slice(0, siteUrl.indexOf('/vti_')); 53 | } 54 | 55 | throw new Error('Unable to resolve web site url from full request url'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/SamlAssertion.ts: -------------------------------------------------------------------------------- 1 | export class SamlAssertion { 2 | public notBefore: string; 3 | public notAfter: string; 4 | public value: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/TokenHelper.ts: -------------------------------------------------------------------------------- 1 | import * as jwt from 'jsonwebtoken'; 2 | import { parse as urlparse } from 'url'; 3 | import { request } from './../config'; 4 | 5 | import { IAppToken } from '../auth/base/IAppToken'; 6 | import { IOnlineAddinCredentials } from '../index'; 7 | import { IAccessToken } from '../auth/base/IAccessToken'; 8 | import * as consts from './../Consts'; 9 | import { IAuthData } from '../auth/base/IAuthData'; 10 | 11 | export class TokenHelper { 12 | public static verifyAppToken(spAppToken: string, oauth: IOnlineAddinCredentials, audience?: string): IAppToken { 13 | const secret = Buffer.from(oauth.clientSecret, 'base64'); 14 | const token = jwt.verify(spAppToken, secret) as IAppToken; 15 | const realm = token.iss.substring(token.iss.indexOf('@') + 1); 16 | const validateAudience = !!audience; 17 | 18 | if (validateAudience) { 19 | const validAudience = `${oauth.clientId}/${audience}@${realm}`; 20 | if (validAudience !== token.aud) { 21 | throw new Error('SP app token validation failed: invalid audience'); 22 | } 23 | } 24 | 25 | token.realm = realm; 26 | token.context = JSON.parse(token.appctx); 27 | return token; 28 | } 29 | 30 | public static getUserAccessToken(spSiteUrl: string, authData: IAuthData, oauth: IOnlineAddinCredentials): Promise { 31 | const spAuthority = urlparse(spSiteUrl).host; 32 | const resource = `${consts.SharePointServicePrincipal}/${spAuthority}@${authData.realm}`; 33 | const appId = `${oauth.clientId}@${authData.realm}`; 34 | const tokenService = urlparse(authData.securityTokenServiceUri); 35 | const tokenUrl = `${tokenService.protocol}//${tokenService.host}/${authData.realm}${tokenService.path}`; 36 | 37 | return request.post(tokenUrl, { 38 | form: { 39 | grant_type: 'refresh_token', 40 | client_id: appId, 41 | client_secret: oauth.clientSecret, 42 | refresh_token: authData.refreshToken, 43 | resource: resource 44 | } 45 | }).json<{access_token: string, expires_on: string}>() 46 | .then(data => { 47 | return { 48 | value: data.access_token, 49 | expireOn: new Date(parseInt(data.expires_on, 10)) 50 | } as IAccessToken; 51 | }); 52 | } 53 | 54 | public static getAppOnlyAccessToken(spSiteUrl: string, authData: IAuthData, oauth: IOnlineAddinCredentials): Promise { 55 | const spAuthority = urlparse(spSiteUrl).host; 56 | const resource = `${consts.SharePointServicePrincipal}/${spAuthority}@${authData.realm}`; 57 | const appId = `${oauth.clientId}@${authData.realm}`; 58 | const tokenService = urlparse(authData.securityTokenServiceUri); 59 | const tokenUrl = `${tokenService.protocol}//${tokenService.host}/${authData.realm}${tokenService.path}`; 60 | 61 | return request.post(tokenUrl, { 62 | form: { 63 | grant_type: 'client_credentials', 64 | client_id: appId, 65 | client_secret: oauth.clientSecret, 66 | scope: resource, 67 | resource: resource 68 | } 69 | }).json<{access_token: string, expires_on: string}>() 70 | .then(data => { 71 | return { 72 | value: data.access_token, 73 | expireOn: new Date(parseInt(data.expires_on, 10)) 74 | } as IAccessToken; 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/UrlHelper.ts: -------------------------------------------------------------------------------- 1 | import * as url from 'url'; 2 | import { HostingEnvironment } from '../auth/HostingEnvironment'; 3 | 4 | export class UrlHelper { 5 | public static removeTrailingSlash(url: string): string { 6 | return url.replace(/(\/$)|(\\$)/, ''); 7 | } 8 | 9 | public static removeLeadingSlash(url: string): string { 10 | return url.replace(/(^\/)|(^\\)/, ''); 11 | } 12 | 13 | public static trimSlashes(url: string): string { 14 | return url.replace(/(^\/)|(^\\)|(\/$)|(\\$)/g, ''); 15 | } 16 | 17 | public static ResolveHostingEnvironment(siteUrl: string): HostingEnvironment { 18 | const host: string = (url.parse(siteUrl)).host; 19 | 20 | if (host.indexOf('.sharepoint.com') !== -1) { 21 | return HostingEnvironment.Production; 22 | } else if (host.indexOf('.sharepoint.cn') !== -1) { 23 | return HostingEnvironment.China; 24 | } else if (host.indexOf('.sharepoint.de') !== -1) { 25 | return HostingEnvironment.German; 26 | } else if (host.indexOf('.sharepoint-mil.us') !== -1) { 27 | return HostingEnvironment.USDefence; 28 | } else if (host.indexOf('.sharepoint.us') !== -1) { 29 | return HostingEnvironment.USGovernment; 30 | } 31 | 32 | return HostingEnvironment.Production; // As default, for O365 Dedicated, #ToInvestigate 33 | // throw new Error('Unable to resolve hosting environment. Site url: ' + siteUrl); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/integration/config.sample.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IUserCredentials, 3 | IOnpremiseUserCredentials, 4 | IOnpremiseFbaCredentials, 5 | IOnPremiseAddinCredentials, 6 | IOnlineAddinCredentials, 7 | IAdfsUserCredentials 8 | } from './../../src/auth/IAuthOptions'; 9 | 10 | export const onlineUrl = '[sharepoint online url]'; 11 | export const onpremAdfsEnabledUrl = '[sharepint on premise url with adfs configured]'; 12 | export const onpremNtlmEnabledUrl = '[sharepint on premise url with ntlm]'; 13 | export const onpremFbaEnabledUrl = '[sharepint on premise url with fba auth]'; 14 | 15 | export const onlineCreds: IUserCredentials = { 16 | username: '[username]', 17 | password: '[password]' 18 | }; 19 | 20 | export const onlineWithAdfsCreds: IUserCredentials = { 21 | username: '[username]', 22 | password: '[password]' 23 | }; 24 | 25 | export const onpremCreds: IOnpremiseUserCredentials = { 26 | username: '[username]', 27 | domain: '[domain]', 28 | password: '[password]' 29 | }; 30 | 31 | export const onpremUpnCreds: IOnpremiseUserCredentials = { 32 | username: '[user@domain.com]', 33 | password: '[password]' 34 | }; 35 | 36 | export const onpremUserWithDomainCreds: IOnpremiseUserCredentials = { 37 | username: '[domain\\user]', 38 | password: '[password]' 39 | }; 40 | 41 | export const onpremFbaCreds: IOnpremiseFbaCredentials = { 42 | username: '[username]', 43 | password: '[password]', 44 | fba: true 45 | }; 46 | 47 | export const onpremAddinOnly: IOnPremiseAddinCredentials = { 48 | clientId: '[clientId]', 49 | issuerId: '[issuerId]', 50 | realm: '[realm]', 51 | rsaPrivateKeyPath: '[rsaPrivateKeyPath]', 52 | shaThumbprint: '[shaThumbprint]' 53 | }; 54 | 55 | export const onlineAddinOnly: IOnlineAddinCredentials = { 56 | clientId: '[clientId]', 57 | clientSecret: '[clientSecret]', 58 | realm: '[realm]' 59 | }; 60 | 61 | export const adfsCredentials: IAdfsUserCredentials = { 62 | username: '[username]', 63 | password: '[password]', 64 | relyingParty: '[relying party]', 65 | adfsUrl: '[adfs url]' 66 | }; 67 | -------------------------------------------------------------------------------- /test/integration/integration.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import got, { Options } from 'got'; 3 | import * as http from 'http'; 4 | import * as https from 'https'; 5 | import * as url from 'url'; 6 | import 'mocha'; 7 | 8 | import { IAuthOptions } from './../../src/auth/IAuthOptions'; 9 | import * as spauth from './../../src/index'; 10 | import { request as configuredRequest } from './../../src/config'; 11 | import { UrlHelper } from '../../src/utils/UrlHelper'; 12 | 13 | interface ITestInfo { 14 | name: string; 15 | creds: IAuthOptions; 16 | url: string; 17 | } 18 | 19 | // eslint-disable-next-line @typescript-eslint/no-var-requires 20 | const config: any = require('./config'); 21 | 22 | const tests: any[] = [ 23 | { 24 | name: 'adfs user credentials', 25 | creds: config.adfsCredentials, 26 | url: config.onpremAdfsEnabledUrl 27 | }, 28 | { 29 | name: 'on-premise user credentials', 30 | creds: config.onpremCreds, 31 | url: config.onpremNtlmEnabledUrl 32 | }, 33 | { 34 | name: 'on-premise user UPN credentials', 35 | creds: config.onpremUpnCreds, 36 | url: config.onpremNtlmEnabledUrl 37 | }, 38 | { 39 | name: 'on-premise user+domain credentials', 40 | creds: config.onpremUserWithDomainCreds, 41 | url: config.onpremNtlmEnabledUrl 42 | }, 43 | { 44 | name: 'online user credentials', 45 | creds: config.onlineCreds, 46 | url: config.onlineUrl 47 | }, 48 | { 49 | name: 'on-premise addin only', 50 | creds: config.onpremAddinOnly, 51 | url: config.onpremAdfsEnabledUrl 52 | }, 53 | { 54 | name: 'online addin only', 55 | creds: config.onlineAddinOnly, 56 | url: config.onlineUrl 57 | }, 58 | { 59 | name: 'ondemand - online', 60 | creds: { 61 | ondemand: true 62 | }, 63 | url: config.onlineUrl 64 | }, 65 | { 66 | name: 'ondemand - on-premise with ADFS', 67 | creds: { 68 | ondemand: true 69 | }, 70 | url: config.onpremAdfsEnabledUrl 71 | }, 72 | { 73 | name: 'file creds - online', 74 | creds: null, 75 | url: config.onlineUrl 76 | }, 77 | { 78 | name: 'file creds - on-premise - NTLM', 79 | creds: null, 80 | url: config.onpremNtlmEnabledUrl 81 | }, 82 | { 83 | name: 'file creds - on-premise - ADFS', 84 | creds: null, 85 | url: config.onpremAdfsEnabledUrl 86 | } 87 | ]; 88 | 89 | tests.forEach(test => { 90 | test.url = UrlHelper.removeTrailingSlash(test.url); 91 | 92 | describe(`node-sp-auth: integration - ${test.name}`, () => { 93 | 94 | it('should get list title with core http(s)', function (done: Mocha.Done): void { 95 | this.timeout(90 * 1000); 96 | 97 | const parsedUrl: url.Url = url.parse(test.url); 98 | const documentTitle = 'Documents'; 99 | const isHttps: boolean = parsedUrl.protocol === 'https:'; 100 | 101 | const send: (options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest = 102 | isHttps ? https.request : http.request; 103 | let agent: http.Agent = isHttps ? new https.Agent({ rejectUnauthorized: false }) : 104 | new http.Agent(); 105 | 106 | spauth.getAuth(test.url, test.creds) 107 | .then(response => { 108 | 109 | const options = getDefaultHeaders(); 110 | const headers: any = Object.assign(options.headers, response.headers); 111 | 112 | if (response.options && response.options['agent']) { 113 | agent = response.options['agent']; 114 | } 115 | 116 | send({ 117 | host: parsedUrl.host, 118 | hostname: parsedUrl.hostname, 119 | port: parseInt(parsedUrl.port, 10), 120 | protocol: parsedUrl.protocol, 121 | path: `${parsedUrl.path}/_api/web/lists/getbytitle('${documentTitle}')`, 122 | method: 'GET', 123 | headers: headers, 124 | agent: agent 125 | }, clientRequest => { 126 | let results = ''; 127 | 128 | clientRequest.on('data', chunk => { 129 | results += chunk; 130 | }); 131 | 132 | clientRequest.on('error', () => { 133 | done(new Error('Unexpected error during http(s) request')); 134 | }); 135 | 136 | clientRequest.on('end', () => { 137 | const data: any = JSON.parse(results); 138 | expect(data.d.Title).to.equal(documentTitle); 139 | done(); 140 | }); 141 | }).end(); 142 | }) 143 | .catch(done); 144 | }); 145 | 146 | it('should get list title', function (done: Mocha.Done): void { 147 | this.timeout(90 * 1000); 148 | const documentTitle = 'Documents'; 149 | 150 | spauth.getAuth(test.url, test.creds) 151 | .then(response => { 152 | const options = getDefaultHeaders(); 153 | Object.assign(options.headers, response.headers); 154 | Object.assign(options, response.options); 155 | options.url = `${test.url}/_api/web/lists/getbytitle('${documentTitle}')`; 156 | 157 | return got.get(options).json(); 158 | }) 159 | .then(data => { 160 | expect((data as any).d.Title).to.equal(documentTitle); 161 | done(); 162 | }) 163 | .catch(done); 164 | }); 165 | 166 | it('should get Title field', function (done: Mocha.Done): void { 167 | this.timeout(90 * 1000); 168 | const fieldTitle = 'Title'; 169 | 170 | spauth.getAuth(test.url, test.creds) 171 | .then(response => { 172 | const options = getDefaultHeaders(); 173 | Object.assign(options.headers, response.headers); 174 | Object.assign(options, response.options); 175 | options.url = `${test.url}/_api/web/fields/getbytitle('${fieldTitle}')`; 176 | 177 | return got(options).json(); 178 | }) 179 | .then(data => { 180 | expect((data as any).d.Title).to.equal(fieldTitle); 181 | done(); 182 | }) 183 | .catch(done); 184 | }); 185 | 186 | it('should throw 500 error', function (done: Mocha.Done): void { 187 | this.timeout(90 * 1000); 188 | 189 | spauth.getAuth(test.url, test.creds) 190 | .then(response => { 191 | const options = getDefaultHeaders(); 192 | Object.assign(options.headers, response.headers); 193 | Object.assign(options, response.options); 194 | const path = UrlHelper.trimSlashes(url.parse(test.url).path); 195 | options.url = `${test.url}/_api/web/GetFileByServerRelativeUrl(@FileUrl)?@FileUrl='/${path}/SiteAssets/${encodeURIComponent('undefined.txt')}'`; 196 | options.retry = 0; 197 | 198 | return got.get(options); 199 | }) 200 | .then(() => { 201 | done(new Error('Should throw')); 202 | }) 203 | .catch(err => { 204 | if (err.message.indexOf('500') !== -1 || err.message.indexOf('404') !== -1) { 205 | done() 206 | } else { 207 | done(err); 208 | } 209 | }); 210 | }); 211 | 212 | it('should not setup custom options for request', function (done: Mocha.Done): void { 213 | spauth.setup({ 214 | requestOptions: { 215 | headers: { 216 | } 217 | } 218 | }); 219 | configuredRequest.get('http://google.com') 220 | .then(result => { 221 | expect(result.headers['my-test-header']).equals(undefined); 222 | done(); 223 | }) 224 | .catch(done); 225 | }); 226 | 227 | it('should setup custom options for request', function (done: Mocha.Done): void { 228 | spauth.setup({ 229 | requestOptions: { 230 | headers: { 231 | 'my-test-header': 'my value' 232 | } 233 | } 234 | }); 235 | 236 | configuredRequest.get('http://google.com') 237 | .then(result => { 238 | expect(result.request.options.headers['my-test-header']).equals('my value'); 239 | done(); 240 | }) 241 | .catch(done); 242 | }); 243 | 244 | }); 245 | }); 246 | 247 | function getDefaultHeaders(): any { 248 | const options: Options = { 249 | responseType: 'json', 250 | headers: { 251 | 'Accept': 'application/json;odata=verbose', 252 | 'Content-Type': 'application/json;odata=verbose' 253 | }, 254 | rejectUnauthorized: false 255 | }; 256 | 257 | return options; 258 | } 259 | -------------------------------------------------------------------------------- /test/integration/spaddin.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEAs1ExIPN/yzyxS5O4xhSg9qYyGXIye4mdGbqqUl6KLAetVi8m 3 | FEgy0eXLNLHuftq9AmkywMXI2knT6T2V4oki55Rwd3PZzjUZ5ZhleOAgvpI8mdy/ 4 | ROLF6XyS6uhppA3tyKDIHuz8aINz+f8axqIK0TikzGwWGVvYTwUHYpcutCpQNABf 5 | TrUHU5aRs/EPQesHp5zkhBOw0L6QjCcVCZqDxRaiLLjHn566fTxglS9fdD69D1GS 6 | LDWGHhl/3J6PkMK8RO8VBebctJkeOi7zEQQci4kuGPgHobh/eRosORUPrIvjqwCO 7 | /3PIN9L9nS0sBsz0DyRy0XtcFV5sWCS+P2FYNgMVJ7aw8Am+GWWIF3J1BrJLRCDa 8 | TZb3QSbrLnWM8ad5ZddU44jehf97YFj4sBT8wfRBHr1dAVFfVIFrbnAtiCMd2MTG 9 | wq6X9M1BVFkErGKQzj3HvEiVvSp7ORhJFSR0C5kghzdYKEaTz64L58M30EdTN0Cp 10 | NoWS/HyKzQ6XkeURUTzXC9ovsGQA77mgDUSnbghWccFQJzCrKgzY/W1NoV0yHyZG 11 | Bh3yvhf4QCHYjrBvTQUfhsKrv2/2JS80joJAsoxIHoqerStJ7me01JD5Q6x0EQNs 12 | lb312yuFsvwjHIbvYoS3jxp7yAWNos1kHAQZi/8jvsHYeHrgJ9zci8xeWXECAwEA 13 | AQKCAgAVg51IXc6sXLjM34lgwqLZVPFX2jqyVb1yk9HRliQvyjAC0h9YeZ0XT+HE 14 | jcCWmbnpqyN6u4AwqIDzT/9GsbttW4Lu4gh01CwqPtGV4hYrpzH8J98O7cJfFCIw 15 | avgZxc4MrzWzeC2EgJkOmovk3xY7KfejyygPnNRkDq9nJcpltVnwE0dcEx4w6jTL 16 | CRYSi5dchddo/U/Oi76eAiZlA5Phtqh4gyb4QLwRQIg5/fgO42QdjWTjRvyzac3M 17 | c324GZUrFCBKl/tM874Lrwxcs0iVeTz9o+yOe6tXpUs/AgbiZdpH7wY5IfQYtojV 18 | LdFFc5uooW/hMFLJAY4wOSBWTGCfzupLfUo2iRkHbO7VZzId8KbosPSgG25rBWDW 19 | zmjF/gfP6f8sSg4leO+wu/t3bXqdstonxtD85uQlOg8I3eMnWcdhr5TeHljh8hDM 20 | jRtO1j7lVtQmvFHOog4kIbVUaxibZt6xhuqPaMoFVEWuzDlUYaY1SN96/CS3uFCC 21 | fJKZiDD4+HChs2zXOXgtR6jK18K4pXhPszI7N1P3OOiVyItcDEVznS6BFAMiL/za 22 | yngTgCAwzoZ0uXn7WmSUuDH513b3vdR+xejfUNPzsm8AHXOOPmgyB5QK6DBMG5jB 23 | U1vlg8ckcWaRMGI/Ih/zE36dkePQsjM5iuyX875uIyyMg6fBPQKCAQEA0HNtn/mG 24 | B55hIpYle6BBQjgyG66kmLMZKofNBY2//+i+bPjm5+/XeO2OfIAJ19yPFLHQd8RG 25 | CCkexdDWfZ33r2Gh6RHBGLghqROGewjjNxb3nkX6B1j6pm11H9snXeIv/8ZmXkMW 26 | cXb7Mk/8NwI4hiI6nqB/2GaurpcwRwJ/9nYFtkX+ZX4VVYPWqJ4aISy0ePwKrsrU 27 | EbH28CnDZNIdnfsZC18oQoDp8jvKRtCIymR9T3A5LZeteoQmqev6ZzZ6MsTfHHJz 28 | MC0Fk7iRcLTjjxx1jfCbKEfY2gSZr0eydyorrKPEXQMW3XFKSTmSYGuuj3CNehZ8 29 | qzOme3L3TuhVBwKCAQEA3Dh83Iu6QKYJAWX4Ji943rW/0RnIs9KUq2wRRdDiUXTq 30 | r4I7jhE9nQNtqOTCd8sFu7f7ckXyaOvYOI0d/U3a+n/nD5X0wDL3m4K6Curj6gLr 31 | TluEk1RuQunZM+35ONiOfLsatRIg/i+tTpvhlo754kApK0WRebWSLJ8ZeOujHOtp 32 | 9BpohHdvz2EKncBlIxpXXDTkC1Pr5TXed227Hp3w9PvWhJRsJkFHZa2GDweyzh9b 33 | fQ8DnSEyRrYJLJtOWl7c70ULEIx959zaCM24OIh+6k9rzVJ8F1E13ETesWM3SE0q 34 | 2F64YRdtAGud6h0nhusWLwK9LNxb+akfjtcldlJ3xwKCAQBNDS3LgW+ip0+eekO2 35 | s+ELejxDcUXUT/eQegw17lS3Yq/pFgQ8XbKXN7CAos+ApD2bV7MIYlvErnZ6hpyS 36 | aG7ivEMeJPrsiTugBOCj4AAlH+896P3n82MLW9B8iwS9Nlupvwud8kx8eo+V5G0F 37 | ZGPCaSqj8g3vztzpGme1B96HGs83th24JGf8aTRStcZQ1vaK9hd4zu6e79qoobdh 38 | MC3UdLmoM29tTbusV5+Il5LIxWZwk7n++V8dt3WXP+wadM+srosON9wORcYW+ZWB 39 | RMwM3WcypWqk9BHbXPH2EZmNZOAp+4sdGoQ8LKFZ+db6nzMyQFd7do50ti3m2fNC 40 | jqzHAoIBAQDZteaKyoBJZVvFzB808P2Xfyqw98KZM/fSOLYixUzYprNU63UhEB5P 41 | WZJRxEYU09tJJ6wn3sq1u2M5FRmu0AdKWqP9nowmbrynOufd3zWOpXAnOQap+HBB 42 | KpqWYg9eiYjj+r1+gPupD01QR38Pry2O5UtOAiq9nilyf59ZEethrcJDls/5FXKu 43 | HAu4xPm3aFUxTQCdykuNgGH8w7iXniEWsNn0nB8G+sYw2QmNVlkIuatiyTMTZjwj 44 | 99a+CJO/d8UHrsQvihT24jmTNn2HNjnyPq4egAs5qgmLR3K4/5MpoVBYM9wn8FbM 45 | cZfeWRA4q7R0qUqITRmIihAu0Leyb/kFAoIBAD+m2NBvobg0thcegDgF27JE+Ghe 46 | O0H3T7P3/FK7c60+rXAwq3mYRzpz/HeqXpa1OuGvNfK262Hq/RwrESYbc7EIKOjs 47 | JH5ecseHE+pTldaRGY7IuDXSRybrYo2CR+6nXx19ibyBJDQ5DmDLwUMUIR1V4gHS 48 | 4ITqhTMiVIxuHRoz0olWYp7osB9UV2Ww/6VJ87Fg3LUHqtJr9qGiEbO3MDuekrKF 49 | eVFhKLWBjbPD+ovCGV4rojuwjInAghxSR52MXyXBtghnaGLWHM0uzcKLYS4OCuKT 50 | xmziph67UU7Iw+xTfykZnLizp8dfT8tdZZIXmZj1TFUj9/ppf+y9HBK5NGI= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /test/integration/tests.ts: -------------------------------------------------------------------------------- 1 | import './integration.spec'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "noImplicitAny": true, 10 | "removeComments": true, 11 | "skipLibCheck": true, 12 | "outDir": "./lib", 13 | "lib": [ 14 | "es2015", 15 | "dom" 16 | ], 17 | "types": [ 18 | "mocha", 19 | "node", 20 | "chai" 21 | ] 22 | }, 23 | "include": [ 24 | "src/**/*.ts", 25 | "test/**/*.ts" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------