Skip to content

Commit 92bea82

Browse files
Integration test password grant (#100)
* test example * created db & model factories * added refresh_token grant type test * removed failing test, not implemented feature * add reference to issue * client authentication test * random client credentials in test * replace math.random by crypto.randomBytes
1 parent 643e091 commit 92bea82

File tree

6 files changed

+716
-0
lines changed

6 files changed

+716
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1
3+
*
4+
* For example (with extra line breaks for display purposes only):
5+
*
6+
* Authorization: Basic czZCaGRSa3F0Mzo3RmpmcDBaQnIxS3REUmJuZlZkbUl3
7+
*
8+
* Alternatively, the authorization server MAY support including the
9+
* client credentials in the request-body using the following
10+
* parameters:
11+
*
12+
* client_id
13+
* REQUIRED. The client identifier issued to the client during
14+
* the registration process described by Section 2.2.
15+
*
16+
* client_secret
17+
* REQUIRED. The client secret. The client MAY omit the
18+
* parameter if the client secret is an empty string.
19+
*/
20+
21+
const OAuth2Server = require('../..');
22+
const DB = require('../helpers/db');
23+
const createModel = require('../helpers/model');
24+
const createRequest = require('../helpers/request');
25+
const Response = require('../../lib/response');
26+
27+
require('chai').should();
28+
29+
const db = new DB();
30+
31+
const auth = new OAuth2Server({
32+
model: createModel(db)
33+
});
34+
35+
const user = db.saveUser({ id: 1, username: 'test', password: 'test'});
36+
const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password'] });
37+
const scope = 'read write';
38+
39+
function createDefaultRequest () {
40+
return createRequest({
41+
body: {
42+
grant_type: 'password',
43+
username: user.username,
44+
password: user.password,
45+
scope
46+
},
47+
headers: {
48+
'authorization': 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'),
49+
'content-type': 'application/x-www-form-urlencoded'
50+
},
51+
method: 'POST',
52+
});
53+
}
54+
55+
describe('Client Authentication Compliance', function () {
56+
describe('No authentication', function () {
57+
it('should be an unsuccesfull authentication', async function () {
58+
const request = createDefaultRequest();
59+
const response = new Response({});
60+
61+
delete request.headers.authorization;
62+
63+
await auth.token(request, response, {})
64+
.then((token) => {
65+
throw new Error('Should not be here');
66+
}).
67+
catch(err => {
68+
err.name.should.equal('invalid_client');
69+
});
70+
});
71+
});
72+
73+
describe('Basic Authentication', function () {
74+
it('should be a succesfull authentication', async function () {
75+
const request = createDefaultRequest();
76+
const response = new Response({});
77+
78+
await auth.token(request, response, {});
79+
});
80+
81+
it('should be an unsuccesfull authentication', async function () {
82+
const request = createDefaultRequest();
83+
const response = new Response({});
84+
85+
request.headers.authorization = 'Basic ' + Buffer.from('a:c').toString('base64');
86+
87+
await auth.token(request, response, {})
88+
.then((token) => {
89+
throw new Error('Should not be here');
90+
}).
91+
catch(err => {
92+
err.name.should.equal('invalid_client');
93+
});
94+
});
95+
});
96+
97+
describe('Request body authentication', function () {
98+
it('should be a succesfull authentication', async function () {
99+
const request = createDefaultRequest();
100+
const response = new Response({});
101+
102+
delete request.headers.authorization;
103+
104+
request.body.client_id = client.id;
105+
request.body.client_secret = client.secret;
106+
107+
await auth.token(request, response, {});
108+
});
109+
110+
it('should be an unsuccesfull authentication', async function () {
111+
const request = createDefaultRequest();
112+
const response = new Response({});
113+
114+
delete request.headers.authorization;
115+
116+
request.body.client_id = 'a';
117+
request.body.client_secret = 'c';
118+
119+
await auth.token(request, response, {})
120+
.then((token) => {
121+
throw new Error('Should not be here');
122+
})
123+
.catch(err => {
124+
err.name.should.equal('invalid_client');
125+
});
126+
});
127+
});
128+
});
+236
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/**
2+
* Request
3+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.3.2
4+
*
5+
* grant_type
6+
* REQUIRED. Value MUST be set to "password".
7+
* username
8+
* REQUIRED. The resource owner username.
9+
* password
10+
* REQUIRED. The resource owner password.
11+
* scope
12+
* OPTIONAL. The scope of the access request as described by Section 3.3.
13+
*/
14+
15+
/**
16+
* Response
17+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
18+
*
19+
* access_token
20+
* REQUIRED. The access token issued by the authorization server.
21+
* token_type
22+
* REQUIRED. The type of the token issued as described in
23+
* Section 7.1. Value is case insensitive.
24+
* expires_in
25+
* RECOMMENDED. The lifetime in seconds of the access token. For
26+
* example, the value "3600" denotes that the access token will
27+
* expire in one hour from the time the response was generated.
28+
* If omitted, the authorization server SHOULD provide the
29+
* expiration time via other means or document the default value.
30+
* refresh_token
31+
* OPTIONAL. The refresh token, which can be used to obtain new
32+
* access tokens using the same authorization grant as described
33+
* in Section 6.
34+
* scope
35+
* OPTIONAL, if identical to the scope requested by the client;
36+
* otherwise, REQUIRED. The scope of the access token as
37+
* described by Section 3.3.
38+
*/
39+
40+
/**
41+
* Response (error)
42+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
43+
*
44+
* error
45+
* REQUIRED. A single ASCII [USASCII] error code from the following:
46+
* invalid_request, invalid_client, invalid_grant
47+
* unauthorized_client, unsupported_grant_type, invalid_scope
48+
* error_description
49+
* OPTIONAL. Human-readable ASCII [USASCII] text providing
50+
* additional information, used to assist the client developer in
51+
* understanding the error that occurred.
52+
* error_uri
53+
* OPTIONAL. A URI identifying a human-readable web page with
54+
* information about the error, used to provide the client
55+
* developer with additional information about the error.
56+
*/
57+
58+
const OAuth2Server = require('../..');
59+
const DB = require('../helpers/db');
60+
const createModel = require('../helpers/model');
61+
const createRequest = require('../helpers/request');
62+
const Response = require('../../lib/response');
63+
const crypto = require('crypto');
64+
65+
require('chai').should();
66+
67+
const db = new DB();
68+
69+
const auth = new OAuth2Server({
70+
model: createModel(db)
71+
});
72+
73+
const user = db.saveUser({ id: 1, username: 'test', password: 'test'});
74+
const client = db.saveClient({ id: 'a', secret: 'b', grants: ['password'] });
75+
const scope = 'read write';
76+
77+
function createDefaultRequest () {
78+
return createRequest({
79+
body: {
80+
grant_type: 'password',
81+
username: user.username,
82+
password: user.password,
83+
scope
84+
},
85+
headers: {
86+
'authorization': 'Basic ' + Buffer.from(client.id + ':' + client.secret).toString('base64'),
87+
'content-type': 'application/x-www-form-urlencoded'
88+
},
89+
method: 'POST',
90+
});
91+
}
92+
93+
describe('PasswordGrantType Compliance', function () {
94+
describe('Authenticate', function () {
95+
it ('Succesfull authorization', async function () {
96+
const request = createDefaultRequest();
97+
const response = new Response({});
98+
99+
const token = await auth.token(request, response, {});
100+
response.body.token_type.should.equal('Bearer');
101+
response.body.access_token.should.equal(token.accessToken);
102+
response.body.refresh_token.should.equal(token.refreshToken);
103+
response.body.expires_in.should.be.a('number');
104+
response.body.scope.should.equal(scope);
105+
106+
token.accessToken.should.be.a('string');
107+
token.refreshToken.should.be.a('string');
108+
token.accessTokenExpiresAt.should.be.a('date');
109+
token.refreshTokenExpiresAt.should.be.a('date');
110+
token.scope.should.equal(scope);
111+
112+
db.accessTokens.has(token.accessToken).should.equal(true);
113+
db.refreshTokens.has(token.refreshToken).should.equal(true);
114+
});
115+
116+
it ('Succesfull authorization and authentication', async function () {
117+
const tokenRequest = createDefaultRequest();
118+
const tokenResponse = new Response({});
119+
120+
const token = await auth.token(tokenRequest, tokenResponse, {});
121+
122+
const authenticationRequest = createRequest({
123+
body: {},
124+
headers: {
125+
'Authorization': `Bearer ${token.accessToken}`
126+
},
127+
method: 'GET',
128+
query: {}
129+
});
130+
const authenticationResponse = new Response({});
131+
132+
const authenticated = await auth.authenticate(
133+
authenticationRequest,
134+
authenticationResponse,
135+
{});
136+
137+
authenticated.scope.should.equal(scope);
138+
authenticated.user.should.be.an('object');
139+
authenticated.client.should.be.an('object');
140+
});
141+
142+
it ('Username missing', async function () {
143+
const request = createDefaultRequest();
144+
const response = new Response({});
145+
146+
delete request.body.username;
147+
148+
await auth.token(request, response, {})
149+
.catch(err => {
150+
err.name.should.equal('invalid_request');
151+
});
152+
});
153+
154+
it ('Password missing', async function () {
155+
const request = createDefaultRequest();
156+
const response = new Response({});
157+
158+
delete request.body.password;
159+
160+
await auth.token(request, response, {})
161+
.catch(err => {
162+
err.name.should.equal('invalid_request');
163+
});
164+
});
165+
166+
it ('Wrong username', async function () {
167+
const request = createDefaultRequest();
168+
const response = new Response({});
169+
170+
request.body.username = 'wrong';
171+
172+
await auth.token(request, response, {})
173+
.catch(err => {
174+
err.name.should.equal('invalid_grant');
175+
});
176+
});
177+
178+
it ('Wrong password', async function () {
179+
const request = createDefaultRequest();
180+
const response = new Response({});
181+
182+
request.body.password = 'wrong';
183+
184+
await auth.token(request, response, {})
185+
.catch(err => {
186+
err.name.should.equal('invalid_grant');
187+
});
188+
});
189+
190+
it ('Client not found', async function () {
191+
const request = createDefaultRequest();
192+
const response = new Response({});
193+
194+
const clientId = crypto.randomBytes(4).toString('hex');
195+
const clientSecret = crypto.randomBytes(4).toString('hex');
196+
197+
request.headers.authorization = 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
198+
199+
await auth.token(request, response, {})
200+
.catch(err => {
201+
err.name.should.equal('invalid_client');
202+
});
203+
});
204+
205+
it ('Client secret not required', async function () {
206+
const request = createDefaultRequest();
207+
const response = new Response({});
208+
209+
delete request.body.client_secret;
210+
211+
const token = await auth.token(request, response, {
212+
requireClientAuthentication: {
213+
password: false
214+
}
215+
});
216+
217+
token.accessToken.should.be.a('string');
218+
});
219+
220+
it ('Client secret required', async function () {
221+
const request = createDefaultRequest();
222+
const response = new Response({});
223+
224+
delete request.body.client_secret;
225+
226+
await auth.token(request, response, {
227+
requireClientAuthentication: {
228+
password: false
229+
}
230+
})
231+
.catch(err => {
232+
err.name.should.equal('invalid_client');
233+
});
234+
});
235+
});
236+
});

0 commit comments

Comments
 (0)