├── .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 |
--------------------------------------------------------------------------------