Skip to content

Commit ba13288

Browse files
authored
Merge pull request #289 from visvk/revoke-handler
#288 Revoke handler
2 parents 6d4c1ce + 6a61aa5 commit ba13288

File tree

6 files changed

+1381
-0
lines changed

6 files changed

+1381
-0
lines changed

lib/handlers/revoke-handler.js

Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
'use strict';
2+
3+
/**
4+
* Module dependencies.
5+
*/
6+
7+
var InvalidArgumentError = require('../errors/invalid-argument-error');
8+
var InvalidClientError = require('../errors/invalid-client-error');
9+
var InvalidTokenError = require('../errors/invalid-token-error');
10+
var InvalidRequestError = require('../errors/invalid-request-error');
11+
var OAuthError = require('../errors/oauth-error');
12+
var Promise = require('bluebird');
13+
var promisify = require('promisify-any');
14+
var Request = require('../request');
15+
var Response = require('../response');
16+
var ServerError = require('../errors/server-error');
17+
var auth = require('basic-auth');
18+
var is = require('../validator/is');
19+
20+
/**
21+
* Constructor.
22+
*/
23+
24+
function RevokeHandler(options) {
25+
options = options || {};
26+
27+
if (!options.model) {
28+
throw new InvalidArgumentError('Missing parameter: `model`');
29+
}
30+
31+
if (!options.model.getClient) {
32+
throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`');
33+
}
34+
35+
if (!options.model.getRefreshToken) {
36+
throw new InvalidArgumentError('Invalid argument: model does not implement `getRefreshToken()`');
37+
}
38+
39+
if (!options.model.getAccessToken) {
40+
throw new InvalidArgumentError('Invalid argument: model does not implement `getAccessToken()`');
41+
}
42+
43+
if (!options.model.revokeToken) {
44+
throw new InvalidArgumentError('Invalid argument: model does not implement `revokeToken()`');
45+
}
46+
47+
this.model = options.model;
48+
}
49+
50+
/**
51+
* Revoke Handler.
52+
*/
53+
54+
RevokeHandler.prototype.handle = function(request, response) {
55+
if (!(request instanceof Request)) {
56+
throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request');
57+
}
58+
59+
if (!(response instanceof Response)) {
60+
throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response');
61+
}
62+
63+
if (request.method !== 'POST') {
64+
return Promise.reject(new InvalidRequestError('Invalid request: method must be POST'));
65+
}
66+
67+
if (!request.is('application/x-www-form-urlencoded')) {
68+
return Promise.reject(new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded'));
69+
}
70+
71+
return Promise.bind(this)
72+
.then(function() {
73+
return this.getClient(request, response);
74+
})
75+
.then(function(client) {
76+
return this.handleRevokeToken(request, client);
77+
})
78+
.catch(function(e) {
79+
if (!(e instanceof OAuthError)) {
80+
e = new ServerError(e);
81+
}
82+
/**
83+
* All necessary information is conveyed in the response code.
84+
*
85+
* Note: invalid tokens do not cause an error response since the client
86+
* cannot handle such an error in a reasonable way. Moreover, the
87+
* purpose of the revocation request, invalidating the particular token,
88+
* is already achieved.
89+
* @see https://tools.ietf.org/html/rfc7009#section-2.2
90+
*/
91+
if (!(e instanceof InvalidTokenError)) {
92+
this.updateErrorResponse(response, e);
93+
}
94+
95+
throw e;
96+
});
97+
};
98+
99+
/**
100+
* Revoke a refresh or access token.
101+
*
102+
* Handle the revoking of refresh tokens, and access tokens if supported / desirable
103+
* RFC7009 specifies that "If the server is unable to locate the token using
104+
* the given hint, it MUST extend its search across all of its supported token types"
105+
*/
106+
107+
RevokeHandler.prototype.handleRevokeToken = function(request, client) {
108+
return Promise.bind(this)
109+
.then(function() {
110+
return this.getTokenFromRequest(request);
111+
})
112+
.then(function(token) {
113+
return Promise.any([
114+
this.getAccessToken(token, client),
115+
this.getRefreshToken(token, client)
116+
])
117+
.catch(Promise.AggregateError, function(err) {
118+
err.forEach(function(e) {
119+
throw e;
120+
});
121+
})
122+
.bind(this)
123+
.tap(function(token) {
124+
return this.revokeToken(token);
125+
});
126+
});
127+
};
128+
129+
/**
130+
* Get the client from the model.
131+
*/
132+
133+
RevokeHandler.prototype.getClient = function(request, response) {
134+
var credentials = this.getClientCredentials(request);
135+
136+
if (!credentials.clientId) {
137+
throw new InvalidRequestError('Missing parameter: `client_id`');
138+
}
139+
140+
if (!credentials.clientSecret) {
141+
throw new InvalidRequestError('Missing parameter: `client_secret`');
142+
}
143+
144+
if (!is.vschar(credentials.clientId)) {
145+
throw new InvalidRequestError('Invalid parameter: `client_id`');
146+
}
147+
148+
if (!is.vschar(credentials.clientSecret)) {
149+
throw new InvalidRequestError('Invalid parameter: `client_secret`');
150+
}
151+
152+
return Promise.try(promisify(this.model.getClient, 2), [credentials.clientId, credentials.clientSecret])
153+
.then(function(client) {
154+
if (!client) {
155+
throw new InvalidClientError('Invalid client: client is invalid');
156+
}
157+
158+
if (!client.grants) {
159+
throw new ServerError('Server error: missing client `grants`');
160+
}
161+
162+
if (!(client.grants instanceof Array)) {
163+
throw new ServerError('Server error: `grants` must be an array');
164+
}
165+
166+
return client;
167+
})
168+
.catch(function(e) {
169+
// Include the "WWW-Authenticate" response header field if the client
170+
// attempted to authenticate via the "Authorization" request header.
171+
//
172+
// @see https://tools.ietf.org/html/rfc6749#section-5.2.
173+
if ((e instanceof InvalidClientError) && request.get('authorization')) {
174+
response.set('WWW-Authenticate', 'Basic realm="Service"');
175+
176+
throw new InvalidClientError(e, { code: 401 });
177+
}
178+
179+
throw e;
180+
});
181+
};
182+
183+
/**
184+
* Get client credentials.
185+
*
186+
* The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively,
187+
* the `client_id` and `client_secret` can be embedded in the body.
188+
*
189+
* @see https://tools.ietf.org/html/rfc6749#section-2.3.1
190+
*/
191+
192+
RevokeHandler.prototype.getClientCredentials = function(request) {
193+
var credentials = auth(request);
194+
195+
if (credentials) {
196+
return { clientId: credentials.name, clientSecret: credentials.pass };
197+
}
198+
199+
if (request.body.client_id && request.body.client_secret) {
200+
return { clientId: request.body.client_id, clientSecret: request.body.client_secret };
201+
}
202+
203+
throw new InvalidClientError('Invalid client: cannot retrieve client credentials');
204+
};
205+
206+
/**
207+
* Get the token from the body.
208+
*
209+
* @see https://tools.ietf.org/html/rfc7009#section-2.1
210+
*/
211+
212+
RevokeHandler.prototype.getTokenFromRequest = function(request) {
213+
var bodyToken = request.body.token;
214+
215+
if (!bodyToken) {
216+
throw new InvalidRequestError('Missing parameter: `token`');
217+
}
218+
219+
return bodyToken;
220+
};
221+
222+
/**
223+
* Get refresh token.
224+
*/
225+
226+
RevokeHandler.prototype.getRefreshToken = function(token, client) {
227+
return Promise.try(promisify(this.model.getRefreshToken, 1), token)
228+
.then(function(token) {
229+
if (!token) {
230+
throw new InvalidTokenError('Invalid token: refresh token is invalid');
231+
}
232+
233+
if (!token.client) {
234+
throw new ServerError('Server error: `getRefreshToken()` did not return a `client` object');
235+
}
236+
237+
if (!token.user) {
238+
throw new ServerError('Server error: `getRefreshToken()` did not return a `user` object');
239+
}
240+
241+
if (token.client.id !== client.id) {
242+
throw new InvalidClientError('Invalid client: client is invalid');
243+
}
244+
245+
if (token.refreshTokenExpiresAt && !(token.refreshTokenExpiresAt instanceof Date)) {
246+
throw new ServerError('Server error: `refreshTokenExpiresAt` must be a Date instance');
247+
}
248+
249+
if (token.refreshTokenExpiresAt && token.refreshTokenExpiresAt < new Date()) {
250+
throw new InvalidTokenError('Invalid token: refresh token has expired');
251+
}
252+
253+
return token;
254+
});
255+
};
256+
257+
/**
258+
* Get the access token from the model.
259+
*/
260+
261+
RevokeHandler.prototype.getAccessToken = function(token, client) {
262+
return Promise.try(promisify(this.model.getAccessToken, 1), token)
263+
.then(function(accessToken) {
264+
if (!accessToken) {
265+
throw new InvalidTokenError('Invalid token: access token is invalid');
266+
}
267+
268+
if (!accessToken.client) {
269+
throw new ServerError('Server error: `getAccessToken()` did not return a `client` object');
270+
}
271+
272+
if (!accessToken.user) {
273+
throw new ServerError('Server error: `getAccessToken()` did not return a `user` object');
274+
}
275+
276+
if (accessToken.client.id !== client.id) {
277+
throw new InvalidClientError('Invalid client: client is invalid');
278+
}
279+
280+
if (accessToken.accessTokenExpiresAt && !(accessToken.accessTokenExpiresAt instanceof Date)) {
281+
throw new ServerError('Server error: `expires` must be a Date instance');
282+
}
283+
284+
if (accessToken.accessTokenExpiresAt && accessToken.accessTokenExpiresAt < new Date()) {
285+
throw new InvalidTokenError('Invalid token: access token has expired.');
286+
}
287+
288+
return accessToken;
289+
});
290+
};
291+
292+
/**
293+
* Revoke the token.
294+
*
295+
* @see https://tools.ietf.org/html/rfc6749#section-6
296+
*/
297+
298+
RevokeHandler.prototype.revokeToken = function(token) {
299+
return Promise.try(promisify(this.model.revokeToken, 1), token)
300+
.then(function(token) {
301+
if (!token) {
302+
throw new InvalidTokenError('Invalid token: token is invalid');
303+
}
304+
305+
return token;
306+
});
307+
};
308+
309+
/**
310+
* Update response when an error is thrown.
311+
*/
312+
313+
RevokeHandler.prototype.updateErrorResponse = function(response, error) {
314+
response.body = {
315+
error: error.name,
316+
error_description: error.message
317+
};
318+
319+
response.status = error.code;
320+
};
321+
322+
/**
323+
* Export constructor.
324+
*/
325+
326+
module.exports = RevokeHandler;

lib/server.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ var AuthenticateHandler = require('./handlers/authenticate-handler');
99
var AuthorizeHandler = require('./handlers/authorize-handler');
1010
var InvalidArgumentError = require('./errors/invalid-argument-error');
1111
var TokenHandler = require('./handlers/token-handler');
12+
var RevokeHandler = require('./handlers/revoke-handler');
1213

1314
/**
1415
* Constructor.
@@ -77,6 +78,18 @@ OAuth2Server.prototype.token = function(request, response, options, callback) {
7778
.nodeify(callback);
7879
};
7980

81+
/**
82+
* Revoke a token.
83+
*/
84+
85+
OAuth2Server.prototype.revoke = function(request, response, options, callback) {
86+
options = _.assign(this.options, options);
87+
88+
return new RevokeHandler(options)
89+
.handle(request, response)
90+
.nodeify(callback);
91+
};
92+
8093
/**
8194
* Export constructor.
8295
*/

0 commit comments

Comments
 (0)