Skip to content

Commit 3fc1f3e

Browse files
committed
revoke-handler: implementation
1 parent 6d4c1ce commit 3fc1f3e

File tree

6 files changed

+1072
-0
lines changed

6 files changed

+1072
-0
lines changed

lib/handlers/revoke-handler.js

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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 InvalidRequestError = require('../errors/invalid-request-error');
10+
var OAuthError = require('../errors/oauth-error');
11+
var Promise = require('bluebird');
12+
var Request = require('../request');
13+
var Response = require('../response');
14+
var ServerError = require('../errors/server-error');
15+
var auth = require('basic-auth');
16+
var is = require('../validator/is');
17+
18+
/**
19+
* Constructor.
20+
*/
21+
22+
function RevokeHandler(options) {
23+
options = options || {};
24+
25+
if (!options.model) {
26+
throw new InvalidArgumentError('Missing parameter: `model`');
27+
}
28+
29+
if (!options.model.getClient) {
30+
throw new InvalidArgumentError('Invalid argument: model does not implement `getClient()`');
31+
}
32+
33+
if (!options.model.getRefreshToken) {
34+
throw new InvalidArgumentError('Invalid argument: model does not implement `getRefreshToken()`');
35+
}
36+
37+
if (!options.model.revokeToken) {
38+
throw new InvalidArgumentError('Invalid argument: model does not implement `revokeToken()`');
39+
}
40+
41+
this.model = options.model;
42+
}
43+
44+
/**
45+
* Revoke Handler.
46+
*/
47+
48+
RevokeHandler.prototype.handle = function(request, response) {
49+
if (!(request instanceof Request)) {
50+
throw new InvalidArgumentError('Invalid argument: `request` must be an instance of Request');
51+
}
52+
53+
if (!(response instanceof Response)) {
54+
throw new InvalidArgumentError('Invalid argument: `response` must be an instance of Response');
55+
}
56+
57+
if (request.method !== 'POST') {
58+
return Promise.reject(new InvalidRequestError('Invalid request: method must be POST'));
59+
}
60+
61+
if (!request.is('application/x-www-form-urlencoded')) {
62+
return Promise.reject(new InvalidRequestError('Invalid request: content must be application/x-www-form-urlencoded'));
63+
}
64+
65+
return Promise.bind(this)
66+
.then(function() {
67+
return this.getClient(request, response);
68+
})
69+
.then(function(client){
70+
return this.handleRevokeToken(request, client);
71+
})
72+
.then(function(){
73+
/**
74+
* All necessary information is conveyed in the response code.
75+
*
76+
* @see https://tools.ietf.org/html/rfc7009#section-2.1
77+
*/
78+
return {};
79+
})
80+
.catch(function(e) {
81+
if (!(e instanceof OAuthError)) {
82+
e = new ServerError(e);
83+
}
84+
85+
this.updateErrorResponse(response, e);
86+
87+
throw e;
88+
});
89+
};
90+
91+
/**
92+
* Handle revoke token
93+
*/
94+
95+
RevokeHandler.prototype.handleRevokeToken = function(request, client) {
96+
return Promise.bind(this)
97+
.then(function() {
98+
return this.getTokenFromRequest(request);
99+
}).then(function (token){
100+
return this.getRefreshToken(token, client);
101+
}).tap(function (token) {
102+
return this.revokeToken(token);
103+
});
104+
};
105+
106+
/**
107+
* Get the client from the model.
108+
*/
109+
110+
RevokeHandler.prototype.getClient = function(request, response) {
111+
var credentials = this.getClientCredentials(request);
112+
113+
if (!credentials.clientId) {
114+
throw new InvalidRequestError('Missing parameter: `client_id`');
115+
}
116+
117+
if (!credentials.clientSecret) {
118+
throw new InvalidRequestError('Missing parameter: `client_secret`');
119+
}
120+
121+
if (!is.vschar(credentials.clientId)) {
122+
throw new InvalidRequestError('Invalid parameter: `client_id`');
123+
}
124+
125+
if (!is.vschar(credentials.clientSecret)) {
126+
throw new InvalidRequestError('Invalid parameter: `client_secret`');
127+
}
128+
129+
return Promise.try(this.model.getClient, [credentials.clientId, credentials.clientSecret])
130+
.then(function(client) {
131+
if (!client) {
132+
throw new InvalidClientError('Invalid client: client is invalid');
133+
}
134+
135+
if (!client.grants) {
136+
throw new ServerError('Server error: missing client `grants`');
137+
}
138+
139+
if (!(client.grants instanceof Array)) {
140+
throw new ServerError('Server error: `grants` must be an array');
141+
}
142+
143+
return client;
144+
})
145+
.catch(function(e) {
146+
// Include the "WWW-Authenticate" response header field if the client
147+
// attempted to authenticate via the "Authorization" request header.
148+
//
149+
// @see https://tools.ietf.org/html/rfc6749#section-5.2.
150+
if ((e instanceof InvalidClientError) && request.get('authorization')) {
151+
response.set('WWW-Authenticate', 'Basic realm="Service"');
152+
153+
throw new InvalidClientError(e, { code: 401 });
154+
}
155+
156+
throw e;
157+
});
158+
};
159+
160+
/**
161+
* Get client credentials.
162+
*
163+
* The client credentials may be sent using the HTTP Basic authentication scheme or, alternatively,
164+
* the `client_id` and `client_secret` can be embedded in the body.
165+
*
166+
* @see https://tools.ietf.org/html/rfc6749#section-2.3.1
167+
*/
168+
169+
RevokeHandler.prototype.getClientCredentials = function(request) {
170+
var credentials = auth(request);
171+
172+
if (credentials) {
173+
return { clientId: credentials.name, clientSecret: credentials.pass };
174+
}
175+
176+
if (request.body.client_id && request.body.client_secret) {
177+
return { clientId: request.body.client_id, clientSecret: request.body.client_secret };
178+
}
179+
180+
throw new InvalidClientError('Invalid client: cannot retrieve client credentials');
181+
};
182+
183+
/**
184+
* Get the token from the body.
185+
*
186+
* @see https://tools.ietf.org/html/rfc7009#section-2.1
187+
*/
188+
189+
RevokeHandler.prototype.getTokenFromRequest = function(request) {
190+
var bodyToken = request.body.token;
191+
192+
if (!bodyToken) {
193+
throw new InvalidRequestError('Missing parameter: `token`');
194+
}
195+
196+
return bodyToken;
197+
};
198+
199+
200+
/**
201+
* Get refresh token.
202+
*/
203+
204+
RevokeHandler.prototype.getRefreshToken = function(token, client) {
205+
206+
return Promise.try(this.model.getRefreshToken, token)
207+
.then(function(token) {
208+
if (!token) {
209+
throw new InvalidRequestError('Invalid request: refresh token is invalid');
210+
}
211+
212+
if (!token.client) {
213+
throw new ServerError('Server error: `getRefreshToken()` did not return a `client` object');
214+
}
215+
216+
if (!token.user) {
217+
throw new ServerError('Server error: `getRefreshToken()` did not return a `user` object');
218+
}
219+
220+
if (token.client.id !== client.id) {
221+
throw new InvalidRequestError('Invalid request: refresh token is invalid');
222+
}
223+
224+
if (token.refreshTokenExpiresAt && !(token.refreshTokenExpiresAt instanceof Date)) {
225+
throw new ServerError('Server error: `refreshTokenExpiresAt` must be a Date instance');
226+
}
227+
228+
if (token.refreshTokenExpiresAt && token.refreshTokenExpiresAt < new Date()) {
229+
throw new InvalidRequestError('Invalid request: refresh token has expired');
230+
}
231+
232+
return token;
233+
});
234+
};
235+
236+
/**
237+
* Revoke the refresh token.
238+
*
239+
*/
240+
241+
RevokeHandler.prototype.revokeToken = function(token) {
242+
return Promise.try(this.model.revokeToken, token)
243+
.then(function(status) {
244+
if (!status) {
245+
throw new InvalidRequestError('Invalid request: refresh token is invalid');
246+
}
247+
248+
return token;
249+
});
250+
};
251+
252+
/**
253+
* Update response when an error is thrown.
254+
*/
255+
256+
RevokeHandler.prototype.updateErrorResponse = function(response, error) {
257+
response.body = {
258+
error: error.name,
259+
error_description: error.message
260+
};
261+
262+
response.status = error.code;
263+
};
264+
265+
/**
266+
* Export constructor.
267+
*/
268+
269+
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)