├── .gitignore ├── lib └── passport-sharepoint │ ├── index.js │ ├── utils.js │ ├── internaloautherror.js │ └── strategy.js ├── versions.md ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.DS_Store 10 | 11 | pids 12 | logs 13 | results 14 | 15 | npm-debug.log 16 | 17 | -------------------------------------------------------------------------------- /lib/passport-sharepoint/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var Strategy = require('./strategy'); 5 | 6 | 7 | /** 8 | * Framework version. 9 | */ 10 | require('pkginfo')(module, 'version'); 11 | 12 | /** 13 | * Expose constructors. 14 | */ 15 | exports.Strategy = Strategy; 16 | -------------------------------------------------------------------------------- /versions.md: -------------------------------------------------------------------------------- 1 | # passport-sharepoint Versions 2 | 3 | ## Version 0.2.10 4 | 5 | - cleaner log messages in case of an auth error 6 | 7 | ## Version 0.2.6 8 | 9 | - support for node 0.10 added 10 | - you must use node 0.10.2 and up because of a TLS handling error in IIS [#5119](https://github.com/joyent/node/issues/5119) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passport-sharepoint", 3 | "description": "SharePoint 2013 OAuth2 strategy für Passport and node.js", 4 | "version": "0.2.12", 5 | "directories": { 6 | "lib": "./lib" 7 | }, 8 | "main": "./lib/passport-sharepoint", 9 | "author": { 10 | "name": "Thomas Herbst", 11 | "email": "thomas.herbst@queport.com" 12 | }, 13 | "dependencies": { 14 | "pkginfo": "0.2.x", 15 | "passport-oauth": "0.1.x", 16 | "oauth": "0.9.x", 17 | "jwt-simple": "0.1.x" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "http://github.com/QuePort/passport-sharepoint.git" 22 | }, 23 | "engines": { "node": ">= 0.4.0" }, 24 | "licenses": [ { 25 | "type": "MIT", 26 | "url": "http://www.opensource.org/licenses/MIT" 27 | } ], 28 | "keywords": ["passport", "oauth", "auth", "authn", "authentication", "sharepoint", "sp2013"] 29 | } 30 | -------------------------------------------------------------------------------- /lib/passport-sharepoint/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reconstructs the original URL of the request. 3 | * 4 | * This function builds a URL that corresponds the original URL requested by the 5 | * client, including the protocol (http or https) and host. 6 | * 7 | * If the request passed through any proxies that terminate SSL, the 8 | * `X-Forwarded-Proto` header is used to detect if the request was encrypted to 9 | * the proxy. 10 | * 11 | * @return {String} 12 | * @api private 13 | */ 14 | exports.originalURL = function(req) { 15 | var headers = req.headers 16 | , protocol = (req.connection.encrypted || req.headers['x-forwarded-proto'] == 'https') 17 | ? 'https' 18 | : 'http' 19 | , host = headers.host 20 | , path = req.url || ''; 21 | return protocol + '://' + host + path; 22 | }; 23 | 24 | /** 25 | * Merge object b with object a. 26 | * 27 | * var a = { foo: 'bar' } 28 | * , b = { bar: 'baz' }; 29 | * 30 | * utils.merge(a, b); 31 | * // => { foo: 'bar', bar: 'baz' } 32 | * 33 | * @param {Object} a 34 | * @param {Object} b 35 | * @return {Object} 36 | * @api private 37 | */ 38 | 39 | exports.merge = function(a, b){ 40 | if (a && b) { 41 | for (var key in b) { 42 | a[key] = b[key]; 43 | } 44 | } 45 | return a; 46 | }; 47 | -------------------------------------------------------------------------------- /lib/passport-sharepoint/internaloautherror.js: -------------------------------------------------------------------------------- 1 | /** 2 | * `InternalOAuthError` error. 3 | * 4 | * InternalOAuthError wraps errors generated by passport-sharepoint. By wrapping these 5 | * objects, error messages can be formatted in a manner that aids in debugging 6 | * OAuth issues. 7 | * 8 | * @api public 9 | */ 10 | function InternalOAuthError(message, err) { 11 | Error.call(this); 12 | Error.captureStackTrace(this, arguments.callee); 13 | this.name = 'InternalOAuthError'; 14 | this.message = message; 15 | this.oauthError = err; 16 | }; 17 | 18 | /** 19 | * Inherit from `Error`. 20 | */ 21 | InternalOAuthError.prototype.__proto__ = Error.prototype; 22 | 23 | /** 24 | * Returns a string representing the error. 25 | * 26 | * @return {String} 27 | * @api public 28 | */ 29 | InternalOAuthError.prototype.toString = function() { 30 | var m = this.message; 31 | if (this.oauthError) { 32 | if (this.oauthError instanceof Error) { 33 | m += ' (' + this.oauthError + ')'; 34 | } 35 | else if (this.oauthError.statusCode && this.oauthError.data) { 36 | m += ' (status: ' + this.oauthError.statusCode + ' data: ' + this.oauthError.data + ')'; 37 | } 38 | } 39 | return m; 40 | } 41 | 42 | 43 | /** 44 | * Expose `InternalOAuthError`. 45 | */ 46 | module.exports = InternalOAuthError; 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Passport-SharePoint 2 | 3 | [Passport](http://passportjs.org/) strategy for authenticating with [SharePoint 2013](http://sharepoint.microsoft.com/.com/) OnPremise and O365 using the OAuth 2.0 API. 4 | 5 | This module lets you authenticate using SharePoint 2013 OnPremise or O365 in your Node.js applications. 6 | By plugging into Passport, SharePoint authentication can be easily and unobtrusively integrated into any application or framework that supports [Connect](http://www.senchalabs.org/connect/)-style middleware, including [Express](http://expressjs.com/). 7 | 8 | ## Installation 9 | 10 | $ npm install passport-sharepoint 11 | 12 | ## Usage 13 | 14 | #### Configure Strategy 15 | 16 | The SharePoint authentication strategy authenticates users using a SharePoint 2013 OnPremise or O365 17 | account using OAuth 2.0. The strategy requires a `verify` callback, which 18 | accepts these credentials and calls `done` providing a user, as well as 19 | `options` specifying a app ID, app secret, and callback URL. 20 | 21 | passport.use(new SharePointStrategy({ 22 | appId: SHAREPOINT_APP_ID , 23 | appSecret: SHAREPOINT_APP_SECRET , 24 | callbackURL: "http://localhost:3000/auth/sharepoint/callback" 25 | }, 26 | function(accessToken, refreshToken, profile, done) { 27 | User.findOrCreate({ userID: profile.id }, function (err, user) { 28 | return done(err, user); 29 | }); 30 | } 31 | )); 32 | 33 | #### Configure SharePoint AppPart 34 | 35 | On the SharePoint side you need a provider hosted AppPart that talks to you Node.JS server and you must register your Node.JS server as a app. 36 | The AppPart you can simply create via the VS2012 AppPart wizard. 37 | These AppPart must define the `StandardTokens` as the url parameter so that the strategy can work. 38 | 39 | 40 | 41 | The Node.JS Server you can register as an app at 42 | `https://sharepoint/_layouts/15/AppRegNew.aspx` 43 | The app id and app secret you specify here is used in our strategy. 44 | 45 | #### App Permission Request 46 | 47 | To load the user profile from the current user automatically, you should add the following permission request to you app manifest or register manually the permission via https://your-tenant.sharepoint.com/_layouts/15/appinv.aspx 48 | 49 | 50 | 51 | 52 | 53 | #### Authenticate Requests 54 | 55 | Use `passport.authenticate()`, specifying the `'sharepoint'` strategy, to 56 | authenticate requests. 57 | 58 | For example, as route middleware in an [Express](http://expressjs.com/) 59 | application: 60 | 61 | app.get('/auth/sharepoint', 62 | passport.authenticate('sharepoint'), 63 | function(req, res){ 64 | // The request will be redirected to SharePoint for authentication, so 65 | // this function will not be called. 66 | }); 67 | 68 | app.get('/auth/sharepoint/callback', 69 | passport.authenticate('sharepoint', { failureRedirect: '/login' }), 70 | function(req, res) { 71 | // Successful authentication, redirect home. 72 | res.redirect('/'); 73 | }); 74 | 75 | ## Credits 76 | 77 | - [QuePort](https://github.com/QuePort) 78 | - [Thomas Herbst](https://github.com/macrauder) 79 | 80 | ## License 81 | 82 | (The MIT License) 83 | 84 | Copyright (c) 2013 Thomas Herbst / QuePort 85 | 86 | Permission is hereby granted, free of charge, to any person obtaining a copy of 87 | this software and associated documentation files (the "Software"), to deal in 88 | the Software without restriction, including without limitation the rights to 89 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 90 | the Software, and to permit persons to whom the Software is furnished to do so, 91 | subject to the following conditions: 92 | 93 | The above copyright notice and this permission notice shall be included in all 94 | copies or substantial portions of the Software. 95 | 96 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 97 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 98 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 99 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 100 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 101 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/passport-sharepoint/strategy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var passport = require('passport') 5 | , url = require('url') 6 | , https = require('https') 7 | , util = require('util') 8 | , utils = require('./utils') 9 | , jwt = require('jwt-simple') 10 | , OAuth2 = require('oauth').OAuth2 11 | , InternalOAuthError = require('./internaloautherror');; 12 | 13 | 14 | var SP_AUTH_PREFIX = '/_layouts/15/OAuthAuthorize.aspx'; 15 | var SP_REDIRECT_PREFIX = '/_layouts/15/appredirect.aspx'; 16 | 17 | /** 18 | * `Strategy` constructor. 19 | * 20 | * @param {Object} options 21 | * @param {Function} verify 22 | * @api public 23 | */ 24 | function Strategy(options, verify) { 25 | options = options || {} 26 | passport.Strategy.call(this); 27 | this.name = 'sharepoint'; 28 | this._verify = verify; 29 | 30 | this._appId = options.appId; 31 | this._appSecret = options.appSecret; 32 | this._callbackURL = options.callbackURL; 33 | this._scope = options.scope; 34 | this._scopeSeparator = options.scopeSeparator || ' '; 35 | this._passReqToCallback = options.passReqToCallback; 36 | this._skipUserProfile = (options.skipUserProfile === undefined) ? false : options.skipUserProfile; 37 | } 38 | 39 | /** 40 | * Inherit from `passport.Strategy`. 41 | */ 42 | util.inherits(Strategy, passport.Strategy); 43 | 44 | 45 | /** 46 | * Authenticate request by delegating to the SharePoint OAuth 2.0 provider. 47 | * 48 | * @param {Object} req 49 | * @api protected 50 | */ 51 | Strategy.prototype.authenticate = function(req, options) { 52 | options = options || {}; 53 | var self = this; 54 | 55 | if (req != undefined && req.query && req.query.error) { 56 | // TODO: Error information pertaining to OAuth 2.0 flows is encoded in the 57 | // query parameters, and should be propagated to the application. 58 | return this.fail(); 59 | } 60 | 61 | var callbackURL = options.callbackURL || this._callbackURL; 62 | if (callbackURL && req != undefined) { 63 | var parsed = url.parse(callbackURL); 64 | if (!parsed.protocol) { 65 | // The callback URL is relative, resolve a fully qualified URL from the 66 | // URL of the originating request. 67 | callbackURL = url.resolve(utils.originalURL(req), callbackURL); 68 | } 69 | } 70 | 71 | var spLanguage = options.spLanguage; 72 | 73 | var spAppToken = undefined; 74 | var spSiteUrl = undefined; 75 | 76 | // load token from request 77 | if (req != undefined && req.body && req.body.SPAppToken) 78 | spAppToken = req.body.SPAppToken; 79 | if(req != undefined && req.body && req.body.SPSiteUrl) 80 | spSiteUrl = req.body.SPSiteUrl; 81 | 82 | if(spSiteUrl == undefined && req != undefined && req.query && req.query.SPHostURL) 83 | spSiteUrl = req.query.SPHostURL; 84 | 85 | //fallback to optional values 86 | if (spAppToken == undefined) 87 | spAppToken = options.spAppToken; 88 | if(spSiteUrl == undefined) 89 | spSiteUrl = options.spSiteUrl; 90 | 91 | // you can pass the appId and Secret in every round 92 | if (options.appId) 93 | this._appId = options.appId; 94 | if (options.appSecret) 95 | this._appSecret = options.appSecret; 96 | 97 | if (!this._appId) throw new Error('SharePointStrategy requires a appId.'); 98 | if (!this._appSecret) throw new Error('SharePointStrategy requires a appSecret.'); 99 | 100 | var authorizationURL = spSiteUrl + SP_AUTH_PREFIX; 101 | var appRedirectURL = spSiteUrl + SP_REDIRECT_PREFIX; 102 | 103 | // check if there is a app token present 104 | if (spAppToken && spSiteUrl) { 105 | var token = eval(jwt.decode(spAppToken, '', true)); 106 | var splitApptxSender = token.appctxsender.split("@"); 107 | var sharepointServer = url.parse(spSiteUrl) 108 | var resource = splitApptxSender[0]+"/"+sharepointServer.host+"@"+splitApptxSender[1]; 109 | var spappId = this._appId+"@"+splitApptxSender[1]; 110 | var appctx = JSON.parse(token.appctx); 111 | var tokenServiceUri = url.parse(appctx.SecurityTokenServiceUri); 112 | var tokenURL = tokenServiceUri.protocol+'//'+tokenServiceUri.host+'/'+splitApptxSender[1]+tokenServiceUri.path; 113 | 114 | this._oauth2 = new OAuth2(spappId, this._appSecret, '', authorizationURL, tokenURL); 115 | this._oauth2.getOAuthAccessToken( 116 | token.refreshtoken, 117 | {grant_type: 'refresh_token', refresh_token: token.refreshtoken, resource: resource}, 118 | function (err, accessToken, refreshToken, params) { 119 | if (err) { return self.error(new InternalOAuthError('failed to obtain access token', err)); } 120 | if (!refreshToken) 121 | refreshToken = spAppToken; 122 | 123 | self._loadUserProfile(accessToken, spSiteUrl, function(err, profile) { 124 | if (err) { return self.error(err); }; 125 | 126 | function verified(err, user, info) { 127 | if (err) { return self.error(err); } 128 | if (!user) { return self.fail(info); } 129 | self.success(user, info); 130 | } 131 | 132 | profile.cacheKey = appctx.CacheKey; 133 | profile.language = spLanguage; 134 | 135 | if (self._passReqToCallback) { 136 | var arity = self._verify.length; 137 | if (arity == 6) { 138 | self._verify(req, accessToken, refreshToken, params, profile, verified); 139 | } else { // arity == 5 140 | self._verify(req, accessToken, refreshToken, profile, verified); 141 | } 142 | } else { 143 | var arity = self._verify.length; 144 | if (arity == 5) { 145 | self._verify(accessToken, refreshToken, params, profile, verified); 146 | } else { // arity == 4 147 | self._verify(accessToken, refreshToken, profile, verified); 148 | } 149 | } 150 | }); 151 | }); 152 | } else if (req != undefined && req.query && req.query.code && authorizationURL && req.query.tokenURL) { 153 | this._oauth2 = new OAuth2(this._appId, this._appSecret, '', authorizationURL, req.query.tokenURL); 154 | var code = req.query.code; 155 | 156 | // NOTE: The module oauth (0.9.5), which is a dependency, automatically adds 157 | // a 'type=web_server' parameter to the percent-encoded data sent in 158 | // the body of the access token request. This appears to be an 159 | // artifact from an earlier draft of OAuth 2.0 (draft 22, as of the 160 | // time of this writing). This parameter is not necessary, but its 161 | // presence does not appear to cause any issues. 162 | this._oauth2.getOAuthAccessToken(code, { grant_type: 'authorization_code', resource: req.query.resource, redirect_uri: callbackURL }, 163 | function(err, accessToken, refreshToken, params) { 164 | if (err) { return self.error(new InternalOAuthError('failed to obtain access token', err)); } 165 | 166 | self._loadUserProfile(accessToken, function(err, profile) { 167 | if (err) { return self.error(err); }; 168 | 169 | function verified(err, user, info) { 170 | if (err) { return self.error(err); } 171 | if (!user) { return self.fail(info); } 172 | self.success(user, info); 173 | } 174 | 175 | if (self._passReqToCallback) { 176 | var arity = self._verify.length; 177 | if (arity == 6) { 178 | self._verify(req, accessToken, refreshToken, params, profile, verified); 179 | } else { // arity == 5 180 | self._verify(req, accessToken, refreshToken, profile, verified); 181 | } 182 | } else { 183 | var arity = self._verify.length; 184 | if (arity == 5) { 185 | self._verify(accessToken, refreshToken, params, profile, verified); 186 | } else { // arity == 4 187 | self._verify(accessToken, refreshToken, profile, verified); 188 | } 189 | } 190 | }); 191 | } 192 | ); 193 | } else if (appRedirectURL) { 194 | this._oauth2 = new OAuth2(this._appId, this._appSecret, '', appRedirectURL, ''); 195 | var params = this.authorizationParams(options); 196 | params['response_type'] = 'code'; 197 | params['redirect_uri'] = callbackURL; 198 | var scope = options.scope || this._scope; 199 | if (scope) { 200 | if (Array.isArray(scope)) { scope = scope.join(this._scopeSeparator); } 201 | params.scope = scope; 202 | } 203 | var state = options.state; 204 | if (state) { params.state = state; } 205 | 206 | var location = this._oauth2.getAuthorizeUrl(params); 207 | this.redirect(location); 208 | } else 209 | return self.error(new InternalOAuthError('failed to obtain access token')); 210 | } 211 | 212 | /** 213 | * Retrieve user profile from SharePoint. 214 | * 215 | * @param {String} accessToken 216 | * @param {Function} done 217 | * @api protected 218 | */ 219 | Strategy.prototype.userProfile = function(accessToken, spSiteUrl, done) { 220 | if (spSiteUrl) 221 | sharepointServer = url.parse(spSiteUrl) 222 | else 223 | return done(null, {}); 224 | if (sharepointServer.path.length > 1) 225 | sharepointServer.path = sharepointServer.path + '/'; 226 | 227 | var headers = { 228 | 'Accept': 'application/json;odata=verbose', 229 | 'Authorization' : 'Bearer ' + accessToken 230 | }; 231 | var options = { 232 | host : sharepointServer.hostname, 233 | port : sharepointServer.port || 443, 234 | path : sharepointServer.path + '_api/web/currentuser', 235 | method : 'GET', 236 | headers : headers, 237 | agent: false, 238 | secureOptions: require('constants').SSL_OP_NO_TLSv1_2 239 | }; 240 | 241 | var req = https.get(options, function(res) { 242 | res.setEncoding('utf8'); 243 | var userData = ''; 244 | 245 | res.on('data', function(data) { 246 | userData += data; 247 | }); 248 | 249 | res.on('end', function() { 250 | var json = JSON.parse(userData); 251 | if (json.d) { 252 | var profile = { provider: 'sharepoint' }; 253 | profile.id = json.d.Id; 254 | profile.username = json.d.LoginName; 255 | profile.displayName = json.d.Title; 256 | profile.emails = [{ value: json.d.Email }]; 257 | siteUrl = url.parse(spSiteUrl); 258 | profile.host = siteUrl.protocol + '//' + siteUrl.host; 259 | profile.site = siteUrl.pathname; 260 | if (profile.site.length > 1) 261 | profile.site = profile.site + '/'; 262 | profile._raw = json; 263 | 264 | done(null, profile); 265 | } else if (json.error) { 266 | return done ('Authentication failed: ' + json.error.code + ' at ' + options.host + ':' + options.port + options.path, null); 267 | } else { 268 | return done('Authentication failed: Unknown exception at' 269 | + options.host + ':' + options.port + options.path , null); 270 | } 271 | }); 272 | }).on('error', function(e) { 273 | return done('Authentication failed: ' + e + ' at ' 274 | + options.host + ':' + options.port + options.path , null); 275 | }); 276 | } 277 | 278 | /** 279 | * Return extra parameters to be included in the authorization request. 280 | * 281 | * @param {Object} options 282 | * @return {Object} 283 | * @api protected 284 | */ 285 | Strategy.prototype.authorizationParams = function(options) { 286 | return {}; 287 | } 288 | 289 | /** 290 | * Load user profile, contingent upon options. 291 | * 292 | * @param {String} accessToken 293 | * @param {Function} done 294 | * @api private 295 | */ 296 | Strategy.prototype._loadUserProfile = function(accessToken, spSiteUrl, done) { 297 | var self = this; 298 | 299 | function loadIt() { 300 | return self.userProfile(accessToken, spSiteUrl, done); 301 | } 302 | function skipIt() { 303 | return done(null); 304 | } 305 | 306 | if (typeof this._skipUserProfile == 'function' && this._skipUserProfile.length > 1) { 307 | // async 308 | this._skipUserProfile(accessToken, function(err, skip) { 309 | if (err) { return done(err); } 310 | if (!skip) { return loadIt(); } 311 | return skipIt(); 312 | }); 313 | } else { 314 | var skip = (typeof this._skipUserProfile == 'function') ? this._skipUserProfile() : this._skipUserProfile; 315 | if (!skip) { return loadIt(); } 316 | return skipIt(); 317 | } 318 | } 319 | 320 | 321 | /** 322 | * Expose `Strategy`. 323 | */ 324 | module.exports = Strategy; 325 | 326 | --------------------------------------------------------------------------------