Skip to content

Commit e718147

Browse files
committed
tests: improve converage for RevokeHandler
1 parent f3b5610 commit e718147

File tree

3 files changed

+311
-27
lines changed

3 files changed

+311
-27
lines changed

lib/handlers/revoke-handler.js

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ class RevokeHandler {
2626
/**
2727
* Constructor.
2828
* @constructor
29-
* @param options
29+
* @param options {object}
30+
* @param options.model {object} An object containing the required model methods.
31+
* @throws {InvalidArgumentError} Thrown if required model methods are missing.
3032
*/
3133
constructor (options) {
3234
options = options || {};
@@ -46,6 +48,14 @@ class RevokeHandler {
4648
this.model = options.model;
4749
}
4850

51+
/**
52+
* The supported token types that may be revoked.
53+
* @return {string[]}
54+
*/
55+
static get TOKEN_TYPES () {
56+
return ['access_token', 'refresh_token'];
57+
}
58+
4959
/**
5060
* Revoke Handler.
5161
*
@@ -67,32 +77,10 @@ class RevokeHandler {
6777

6878
try {
6979
const client = await this.getClient(request, response);
70-
71-
if (!client) {
72-
throw new InvalidClientError('Invalid client: client is invalid');
73-
}
74-
75-
const token = request.body.token;
76-
77-
// An invalid token type hint value is ignored by the authorization
78-
// server and does not influence the revocation response.
79-
const tokenTypeHint = request.body.token_type_hint;
80-
81-
if (!token) {
82-
throw new InvalidRequestError('Missing parameter: `token`');
83-
}
84-
85-
if (!isFormat.vschar(token)) {
86-
throw new InvalidRequestError('Invalid parameter: `token`');
87-
}
88-
89-
// Validate token_type_hint if provided
90-
if (tokenTypeHint && tokenTypeHint !== 'access_token' && tokenTypeHint !== 'refresh_token') {
91-
throw new UnsupportedTokenTypeError('Unsupported token_type_hint: ' + tokenTypeHint);
92-
}
80+
const { token, tokenTypeHint } = this.getToken(request);
9381

9482
// Try to find and revoke the token
95-
await this.revokeToken(token, tokenTypeHint, client);
83+
await this.revokeToken({ token, tokenTypeHint, client });
9684

9785
// Per RFC 7009 section 2.2: return 200 OK even if token was invalid
9886
// This prevents token enumeration attacks
@@ -128,7 +116,7 @@ class RevokeHandler {
128116
}
129117

130118
/**
131-
* Get the client from the model.
119+
* Get the client from the model and validate it.
132120
*/
133121

134122
async getClient (request, response) {
@@ -191,6 +179,32 @@ class RevokeHandler {
191179
throw new InvalidClientError('Invalid client: cannot retrieve client credentials');
192180
}
193181

182+
/**
183+
* Extract and validate token from request
184+
* @param request {Request}
185+
* @return {{ token: string, token_type_hint: string|undefined}} An object containing the token and token type hint.
186+
*/
187+
getToken (request) {
188+
const token = request.body.token;
189+
190+
if (!token) {
191+
throw new InvalidRequestError('Missing parameter: `token`');
192+
}
193+
194+
if (!isFormat.vschar(token)) {
195+
throw new InvalidRequestError('Invalid parameter: `token`');
196+
}
197+
198+
// An invalid token type hint value is ignored by the authorization
199+
// server and does not influence the revocation response.
200+
let tokenTypeHint = request.body.token_type_hint;
201+
if (!RevokeHandler.TOKEN_TYPES.includes(tokenTypeHint)) {
202+
tokenTypeHint = undefined;
203+
}
204+
205+
return { token, tokenTypeHint };
206+
}
207+
194208
/**
195209
* Revoke the token.
196210
*
@@ -199,7 +213,7 @@ class RevokeHandler {
199213
* token enumeration attacks.
200214
*/
201215

202-
async revokeToken (token, tokenTypeHint, client) {
216+
async revokeToken ({ token, tokenTypeHint, client }) {
203217
let tokenToRevoke = null;
204218

205219
// Try to find the token based on the hint

lib/server.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,12 @@ class OAuth2Server {
235235
return new TokenHandler(options).handle(request, response);
236236
}
237237

238+
/**
239+
* Revokes an access or refresh token, as defined in RFC 7009.
240+
* @param request {Request}
241+
* @param response {Response}
242+
* @return {Promise<void>}
243+
*/
238244
revoke (request, response) {
239245
return new RevokeHandler(this.options).handle(request, response);
240246
}
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
'use strict';
2+
3+
/*
4+
* Module dependencies.
5+
*/
6+
const Request = require('../../../lib/request');
7+
const Model = require('../../../lib/model');
8+
const RevokeHandler = require('../../../lib/handlers/revoke-handler');
9+
const sinon = require('sinon');
10+
const InvalidArgumentError = require('../../../lib/errors/invalid-argument-error');
11+
const InvalidClientError = require('../../../lib/errors/invalid-client-error')
12+
const InvalidRequestError = require('../../../lib/errors/invalid-request-error')
13+
const should = require('chai').should();
14+
15+
/**
16+
* Test `TokenHandler`.
17+
*/
18+
19+
describe('RevokeHandler', () => {
20+
describe('constructor()', () => {
21+
it('should throw an error if `model` is missing', () => {
22+
(() => {
23+
new RevokeHandler({});
24+
}).should.throw(InvalidArgumentError, 'Missing parameter: `model`');
25+
});
26+
it('should throw an error if `model` does not implement `getClient()`', () => {
27+
(() => {
28+
new RevokeHandler({ model: {} });
29+
}).should.throw(InvalidArgumentError, 'Invalid argument: model does not implement `getClient()`');
30+
});
31+
it('should throw an error if `model` does not implement `revokeToken()`', () => {
32+
(() => {
33+
new RevokeHandler({
34+
model: {
35+
getClient: () => {}
36+
}
37+
});
38+
}).should.throw(InvalidArgumentError, 'Invalid argument: model does not implement `revokeToken()`');
39+
});
40+
});
41+
describe('getClient()', () => {
42+
it('should call `model.getClient()`', async () => {
43+
const model = Model.from({
44+
getClient: sinon.stub().returns({ grants: ['password'] }),
45+
saveToken: () => {
46+
},
47+
revokeToken: () => {
48+
}
49+
});
50+
const handler = new RevokeHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 });
51+
const request = new Request({
52+
body: { client_id: 12345, client_secret: 'secret' },
53+
headers: {},
54+
method: {},
55+
query: {}
56+
});
57+
58+
await handler.getClient(request);
59+
model.getClient.callCount.should.equal(1);
60+
model.getClient.firstCall.args.should.have.length(2);
61+
model.getClient.firstCall.args[0].should.equal(12345);
62+
model.getClient.firstCall.args[1].should.equal('secret');
63+
model.getClient.firstCall.thisValue.should.equal(model);
64+
});
65+
it('throws an error if the client is invalid', async () => {
66+
const model = Model.from({
67+
getClient: sinon.stub().returns(null),
68+
saveToken: () => {
69+
},
70+
revokeToken: () => {
71+
}
72+
});
73+
const handler = new RevokeHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 });
74+
const request = new Request({
75+
body: { client_id: 12345, client_secret: 'secret' },
76+
headers: {},
77+
method: {},
78+
query: {}
79+
});
80+
81+
try {
82+
await handler.getClient(request);
83+
should.fail();
84+
} catch (e) {
85+
e.should.be.instanceOf(InvalidClientError);
86+
e.message.should.equal('Invalid client: client is invalid');
87+
}
88+
});
89+
it('throws an error if client id is using invalid chars', async () => {
90+
const model = Model.from({
91+
getClient: sinon.stub().returns({ grants: ['password'] }),
92+
saveToken: () => {
93+
},
94+
revokeToken: () => {
95+
}
96+
});
97+
const handler = new RevokeHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 });
98+
const request = new Request({
99+
body: { client_id: '12😵345', client_secret: 'secret' },
100+
headers: {},
101+
method: {},
102+
query: {}
103+
});
104+
105+
try {
106+
await handler.getClient(request);
107+
should.fail();
108+
} catch (e) {
109+
e.should.be.instanceOf(InvalidRequestError);
110+
e.message.should.equal('Invalid parameter: `client_id`');
111+
}
112+
});
113+
it ('throws an error if client_secret is usind invalid chars', async () => {
114+
const model = Model.from({
115+
getClient: sinon.stub().returns({ grants: ['password'] }),
116+
saveToken: () => {
117+
},
118+
revokeToken: () => {
119+
}
120+
});
121+
const handler = new RevokeHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 });
122+
const request = new Request({
123+
body: { client_id: '12345', client_secret: 'sec😵ret' },
124+
headers: {},
125+
method: {},
126+
query: {}
127+
});
128+
129+
try {
130+
await handler.getClient(request);
131+
should.fail();
132+
} catch (e) {
133+
e.should.be.instanceOf(InvalidRequestError);
134+
e.message.should.equal('Invalid parameter: `client_secret`');
135+
}
136+
});
137+
});
138+
describe('getClientCredentials()', () => {
139+
it('should throw an error if client credentials are missing on confidential clients', async () => {
140+
const model = Model.from({
141+
getClient: sinon.stub().returns({ grants: ['client_credentials'] }),
142+
saveToken: () => {
143+
},
144+
revokeToken: () => {
145+
}
146+
});
147+
const handler = new RevokeHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 });
148+
const request = new Request({ body: {}, headers: {}, method: {}, query: {} });
149+
150+
try {
151+
await handler.getClientCredentials(request);
152+
should.fail();
153+
} catch (e) {
154+
e.should.be.instanceOf(InvalidClientError);
155+
e.message.should.equal('Invalid client: cannot retrieve client credentials');
156+
}
157+
});
158+
});
159+
describe('updateSuccessResponse', () => {
160+
it('updates the response with success information', () => {
161+
const model = Model.from({
162+
getClient: sinon.stub().returns({ grants: ['password'] }),
163+
saveToken: () => {
164+
},
165+
revokeToken: () => {
166+
}
167+
});
168+
const handler = new RevokeHandler({ accessTokenLifetime: 120, model: model, refreshTokenLifetime: 120 });
169+
const response = {
170+
set: sinon.spy()
171+
};
172+
173+
handler.updateSuccessResponse(response);
174+
response.body.should.deep.equal({});
175+
response.status.should.equal(200);
176+
response.set.callCount.should.equal(2);
177+
response.set.firstCall.args.should.have.length(2);
178+
response.set.firstCall.args[0].should.equal('Cache-Control');
179+
response.set.firstCall.args[1].should.equal('no-store');
180+
response.set.secondCall.args.should.have.length(2);
181+
response.set.secondCall.args[0].should.equal('Pragma');
182+
response.set.secondCall.args[1].should.equal('no-cache');
183+
});
184+
});
185+
describe('updateErrorResponse', () => {
186+
it('updates the response with error information', () => {
187+
const model = Model.from({
188+
getClient: sinon.stub().returns({ grants: ['password'] }),
189+
saveToken: () => {
190+
},
191+
revokeToken: () => {
192+
}
193+
});
194+
const handler = new RevokeHandler({ model: model });
195+
const response = {
196+
set: sinon.spy()
197+
};
198+
199+
handler.updateErrorResponse(response, new InvalidRequestError('Invalid request 123'));
200+
response.body.should.deep.equal({
201+
error: 'invalid_request',
202+
error_description: 'Invalid request 123'
203+
});
204+
response.status.should.equal(400);
205+
});
206+
});
207+
describe('getToken', () => {
208+
const model = Model.from({
209+
getClient: () => {},
210+
saveToken: () => {},
211+
revokeToken: () => {}
212+
});
213+
const handler = new RevokeHandler({ model});
214+
215+
it('throws an error if the token is missing');
216+
it('throws an error if the token is in an invalid format', () => {
217+
const missing = [false, null, undefined, '', 0];
218+
missing.forEach((token) => {
219+
try {
220+
handler.getToken({ body: { token } });
221+
should.fail();
222+
} catch (e) {
223+
e.should.be.instanceOf(InvalidRequestError);
224+
e.message.should.equal('Missing parameter: `token`');
225+
}
226+
});
227+
const invalid = ['123❤️45', {}, [], true];
228+
invalid.forEach((token) => {
229+
try {
230+
handler.getToken({ body: { token } });
231+
should.fail();
232+
} catch (e) {
233+
e.should.be.instanceOf(InvalidRequestError);
234+
e.message.should.equal('Invalid parameter: `token`');
235+
}
236+
});
237+
});
238+
it('returns the token and token_type_hint from the request body, if defined and valid', () => {
239+
240+
const token = '123445';
241+
const invalid = [undefined, null, 'invalid', 'refresh-token', 'access-token'];
242+
invalid.forEach((tokenTypeHint) => {
243+
const result = handler.getToken({ body: { token, token_type_hing: tokenTypeHint } });
244+
result.token.should.equal(token);
245+
should.equal(result.tokenTypeHint, undefined);
246+
});
247+
248+
const valid = ['access_token', 'refresh_token'];
249+
valid.forEach((tokenTypeHint) => {
250+
const result = handler.getToken({ body: { token, token_type_hint: tokenTypeHint } });
251+
result.token.should.equal(token);
252+
result.tokenTypeHint.should.equal(tokenTypeHint);
253+
});
254+
});
255+
});
256+
describe('revokeToken', () => {
257+
it('it ignores invalid token_type_hint values');
258+
it('it revokes access tokens when token_type_hint is access_token');
259+
it('it revokes refresh tokens when token_type_hint is refresh_token');
260+
it('it revokes tokens without a token_type_hint');
261+
it('it does not revoke tokens belonging to other clients');
262+
it('it does not throw an error if the token to be revoked is not found');
263+
});
264+
});

0 commit comments

Comments
 (0)