Gitter migration: Setup redirects (rollout pt. 3)
[gitter.git] / server / web / oauth2.js
blobb6e48926eed12df015ae4a0d05e3153d2b773764
1 'use strict';
3 /**
4  * Module dependencies.
5  */
6 var env = require('gitter-web-env');
7 var logger = env.logger;
8 var errorReporter = env.errorReporter;
9 var stats = env.stats;
10 const url = require('url');
12 var oauth2orize = require('oauth2orize');
13 var passport = require('passport');
14 var oauthService = require('gitter-web-oauth');
15 var random = require('gitter-web-oauth/lib/random');
16 var ensureLoggedIn = require('./middlewares/ensure-logged-in');
18 const OauthAuthorizationError = require('./oauth-authorization-error');
20 // create OAuth 2.0 server
21 var server = oauth2orize.createServer();
23 // Register serialialization and deserialization functions.
25 // When a client redirects a user to user authorization endpoint, an
26 // authorization transaction is initiated.  To complete the transaction, the
27 // user must authenticate and approve the authorization request.  Because this
28 // may involve multiple HTTP request/response exchanges, the transaction is
29 // stored in the session.
31 // An application must supply serialization functions, which determine how the
32 // client object is serialized into the session.  Typically this will be a
33 // simple matter of serializing the client's Id, and deserializing by finding
34 // the client by Id from the database.
36 server.serializeClient(function(client, done) {
37   return done(null, client.id);
38 });
40 server.deserializeClient(function(id, done) {
41   oauthService.findClientById(id, done);
42 });
44 // Register supported grant types.
46 // OAuth 2.0 specifies a framework that allows users to grant client
47 // applications limited access to their protected resources.  It does this
48 // through a process of the user granting access, and the client exchanging
49 // the grant for an access token.
51 // Grant authorization codes.  The callback takes the `client` requesting
52 // authorization, the `redirectUri` (which is used as a verifier in the
53 // subsequent exchange), the authenticated `user` granting access, and
54 // their response, which contains approved scope, duration, etc. as parsed by
55 // the application.  The application issues a code, which is bound to these
56 // values, and will be exchanged for an access token.
58 server.grant(
59   oauth2orize.grant.code(function(client, redirectUri, user, ares, done) {
60     logger.info('Granted access to ' + client.name + ' for ' + user.displayName);
62     random.generateToken(function(err, token) {
63       if (err) {
64         return done(err);
65       }
67       oauthService.saveAuthorizationCode(token, client, redirectUri, user, function(err) {
68         if (err) {
69           return done(err);
70         }
71         done(null, token);
72       });
73     });
74   })
77 // Exchange authorization codes for access tokens.  The callback accepts the
78 // `client`, which is exchanging `code` and any `redirectUri` from the
79 // authorization request for verification.  If these values are validated, the
80 // application issues an access token on behalf of the user who authorized the
81 // code.
83 server.exchange(
84   oauth2orize.exchange.code(async function(client, code, redirectUri, done) {
85     try {
86       const authCode = await oauthService.findAuthorizationCode(code);
87       if (!authCode) return done();
89       if (!client._id.equals(authCode.clientId)) {
90         return done();
91       }
92       if (redirectUri !== authCode.redirectUri) {
93         return done();
94       }
96       const token = await oauthService.findOrCreateToken(authCode.userId, authCode.clientId);
97       // > The client MUST NOT use the authorization code more than once.
98       // > https://tools.ietf.org/html/rfc6749#section-4.1.2
99       await oauthService.deleteAuthorizationCode(code);
100       done(null, token);
101     } catch (err) {
102       if (err) return done(err);
103     }
104   })
107 // user authorization endpoint
109 // `authorization` middleware accepts a `validate` callback which is
110 // responsible for validating the client making the authorization request.  In
111 // doing so, is recommended that the `redirectUri` be checked against a
112 // registered value, although security requirements may vary across
113 // implementations.  Once validated, the `done` callback must be invoked with
114 // a `client` instance, as well as the `redirectUri` to which the user will be
115 // redirected after an authorization decision is obtained.
117 // This middleware simply initializes a new authorization transaction.  It is
118 // the application's responsibility to authenticate the user and render a dialog
119 // to obtain their approval (displaying details about the client requesting
120 // authorization).  We accomplish that here by routing through `ensureLoggedIn()`
121 // first, and rendering the `dialog` view.
123 exports.authorization = [
124   ensureLoggedIn,
125   server.authorization(async function(clientKey, redirectUri, done) {
126     try {
127       stats.event('oauth.authorize');
128       const client = await oauthService.findClientByClientKey(clientKey);
130       if (!client) {
131         return done(new OauthAuthorizationError('Provided clientKey does not exist.'));
132       }
134       if (
135         !client.registeredRedirectUri ||
136         !redirectUri ||
137         client.registeredRedirectUri !== redirectUri
138       ) {
139         logger.warn('Provided redirectUri does not match registered URI for client_id/clientKey ', {
140           redirectUri: redirectUri,
141           registeredUri: client.registeredRedirectUri,
142           clientKey: clientKey
143         });
145         return done(
146           new OauthAuthorizationError(
147             'Provided redirectUri does not match registered URI for client_id/clientKey'
148           )
149         );
150       }
152       const urlData = url.parse(client.registeredRedirectUri);
153       const hasBadProtocol =
154         !urlData.protocol || urlData.protocol === 'javascript:' || urlData.protocol === 'data:';
155       if (hasBadProtocol) {
156         logger.warn('Provided redirectUri is using disallowed bad protocol ', {
157           redirectUri: redirectUri,
158           registeredUri: client.registeredRedirectUri,
159           clientKey: clientKey
160         });
162         return done(
163           new OauthAuthorizationError(
164             'Provided redirectUri is using disallowed bad protocol (no javascript:// or data://)'
165           )
166         );
167       }
169       return done(null, client, redirectUri);
170     } catch (err) {
171       errorReporter(err, { clientKey, redirectUri }, { module: 'oauth.authorize' });
172       done(new OauthAuthorizationError('Error occured while oauth.authorize'));
173     }
174   }),
175   function(req, res, next) {
176     /* Is this client allowed to skip the authorization page? */
177     if (req.oauth2.client.canSkipAuthorization) {
178       return server.decision({ loadTransaction: false })(req, res, next);
179     }
181     stats.event('oauth.authorize.dialog');
183     /* Non-trusted Client */
184     res.render('oauth_authorize_dialog', {
185       transactionId: req.oauth2.transactionID,
186       user: req.user,
187       client: req.oauth2.client
188     });
189   },
190   function(err, req, res, next) {
191     stats.event('oauth.authorize.failed');
192     errorReporter(err, { oauthAuthorizationDialog: 'failed' }, { module: 'oauth2' });
194     var missingParams = ['response_type', 'redirect_uri', 'client_id'].filter(function(param) {
195       return !req.query[param];
196     });
198     var incorrectResponseType = req.query.response_type && req.query.response_type !== 'code';
200     if (err instanceof OauthAuthorizationError || missingParams.length || incorrectResponseType) {
201       res.status(401);
202       res.render('oauth_authorize_failed', {
203         errorMessage: err.message,
204         missingParams: missingParams.length && missingParams,
205         incorrectResponseType: incorrectResponseType
206       });
207     } else {
208       /* Let the main error handler deal with this */
209       next();
210     }
211   }
214 // user decision endpoint
216 // `decision` middleware processes a user's decision to allow or deny access
217 // requested by a client application.  Based on the grant type requested by the
218 // client, the above grant middleware configured above will be invoked to send
219 // a response.
221 exports.decision = [ensureLoggedIn, server.decision()];
223 // token endpoint
225 // `token` middleware handles client requests to exchange authorization grants
226 // for access tokens.  Based on the grant type being exchanged, the above
227 // exchange middleware will be invoked to handle the request.  Clients must
228 // authenticate when making requests to this endpoint.
230 exports.token = [
231   function(req, res, next) {
232     stats.event('oauth.exchange');
233     next();
234   },
235   passport.authenticate([/*'basic', */ 'oauth2-client-password'], {
236     session: false,
237     failWithError: true
238   }),
239   server.token(),
240   server.errorHandler()