Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@ declare namespace OAuth2Server {
grants: string | string[];
accessTokenLifetime?: number;
refreshTokenLifetime?: number;
type?: 'public' | 'confidential';
[key: string]: any;
}

Expand Down
20 changes: 10 additions & 10 deletions lib/handlers/token-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,6 @@ class TokenHandler {
throw new InvalidRequestError('Missing parameter: `client_id`');
}

if (this.isClientAuthenticationRequired(grantType) && !credentials.clientSecret && !isPkce) {
throw new InvalidRequestError('Missing parameter: `client_secret`');
}

if (!isFormat.vschar(credentials.clientId)) {
throw new InvalidRequestError('Invalid parameter: `client_id`');
}
Expand All @@ -136,7 +132,7 @@ class TokenHandler {
}

try {
const client = await this.model.getClient(credentials.clientId, credentials.clientSecret);
const client = await this.model.getClient(credentials.clientId, credentials.clientSecret ?? null);
Comment thread
dsschiramm marked this conversation as resolved.
Outdated

if (!client) {
throw new InvalidClientError('Invalid client: client is invalid');
Expand All @@ -150,6 +146,10 @@ class TokenHandler {
throw new ServerError('Server error: `grants` must be an array');
}

if (this.isClientAuthenticationRequired(grantType, client) && !credentials.clientSecret && !isPkce) {
throw new InvalidClientError('Invalid client: client is invalid');
}

return client;
} catch (e) {
// Include the "WWW-Authenticate" response header field if the client
Expand Down Expand Up @@ -196,10 +196,8 @@ class TokenHandler {
}
}
Comment thread
dsschiramm marked this conversation as resolved.
Outdated

if (!this.isClientAuthenticationRequired(grantType)) {
if (request.body.client_id) {
return { clientId: request.body.client_id };
}
if (request.body.client_id) {
return { clientId: request.body.client_id };
}

throw new InvalidClientError('Invalid client: cannot retrieve client credentials');
Expand Down Expand Up @@ -307,7 +305,9 @@ class TokenHandler {
/**
* Given a grant type, check if client authentication is required
*/
isClientAuthenticationRequired(grantType) {
isClientAuthenticationRequired(grantType, client) {
if (client && client.type === 'public') return false;

if (Object.keys(this.requireClientAuthentication).length > 0) {
return typeof this.requireClientAuthentication[grantType] !== 'undefined'
? this.requireClientAuthentication[grantType]
Expand Down
1 change: 1 addition & 0 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const ServerError = require('./errors/server-error');
* @property grants {string[]} Grant types allowed for the client.
* @property accessTokenLifetime {number} Client-specific lifetime of generated access tokens in seconds.
* @property refreshTokenLifetime {number} Client-specific lifetime of generated refresh tokens in seconds.
* @property type {string} The client type: `public` or `confidential`. Defaults to 'confidential' when not set.
*/

/**
Expand Down
104 changes: 95 additions & 9 deletions test/integration/handlers/token-handler_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,98 @@ describe('TokenHandler integration', function () {
.catch(should.fail);
});

describe('with a public client and no `client_secret`', function () {
it('should return the client without requiring a secret', function () {
const client = { id: 'foo', grants: ['authorization_code'], type: 'public' };
const model = Model.from({
getClient: function () {
return client;
},
saveToken: function () {},
});
const handler = new TokenHandler({
accessTokenLifetime: 120,
model: model,
refreshTokenLifetime: 120,
});
const request = new Request({
body: { client_id: 'foo', grant_type: 'authorization_code' },
headers: {},
method: {},
query: {},
});

return handler
.getClient(request)
.then(function (data) {
data.should.equal(client);
})
.catch(should.fail);
});
});

describe('with a confidential client (explicit type) and no `client_secret`', function () {
it('should throw an error', function () {
const client = { id: 'foo', grants: ['authorization_code'], type: 'confidential' };
const model = Model.from({
getClient: function () {
return client;
},
saveToken: function () {},
});
const handler = new TokenHandler({
accessTokenLifetime: 120,
model: model,
refreshTokenLifetime: 120,
});
const request = new Request({
body: { client_id: 'foo', grant_type: 'authorization_code' },
headers: {},
method: {},
query: {},
});

return handler
.getClient(request)
.then(should.fail)
.catch(function (e) {
e.should.be.an.instanceOf(InvalidClientError);
e.message.should.equal('Invalid client: client is invalid');
});
});
});

describe('with a confidential client (no type, default) and no `client_secret`', function () {
it('should throw an error', function () {
const client = { id: 'foo', grants: ['password'] };
const model = Model.from({
getClient: function () {
return client;
},
saveToken: function () {},
});
const handler = new TokenHandler({
accessTokenLifetime: 120,
model: model,
refreshTokenLifetime: 120,
});
const request = new Request({
body: { client_id: 'foo', grant_type: 'password' },
headers: {},
method: {},
query: {},
});

return handler
.getClient(request)
.then(should.fail)
.catch(function (e) {
e.should.be.an.instanceOf(InvalidClientError);
e.message.should.equal('Invalid client: client is invalid');
});
});
});

describe('with `password` grant type and `requireClientAuthentication` is false', function () {
it('should return a client ', function () {
const client = { id: 12345, grants: [] };
Expand Down Expand Up @@ -909,7 +1001,7 @@ describe('TokenHandler integration', function () {
}
});

it('should throw an error if `client_secret` is missing', async function () {
it('should return credentials with only `clientId` when `client_secret` is missing', function () {
const model = Model.from({
getClient: function () {},
saveToken: function () {},
Expand All @@ -926,14 +1018,8 @@ describe('TokenHandler integration', function () {
query: {},
});

try {
await handler.getClientCredentials(request);

should.fail();
} catch (e) {
e.should.be.an.instanceOf(InvalidClientError);
e.message.should.equal('Invalid client: cannot retrieve client credentials');
}
const credentials = handler.getClientCredentials(request);
credentials.should.eql({ clientId: 'foo' });
});

describe('with `client_id` and grant type is `password` and `requireClientAuthentication` is false', function () {
Expand Down
29 changes: 28 additions & 1 deletion test/unit/handlers/token-handler_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const should = require('chai').should();

describe('TokenHandler', function () {
describe('getClient()', function () {
it('should call `model.getClient()`', function () {
it('should call `model.getClient()` with the provided secret', function () {
const model = Model.from({
getClient: sinon.stub().returns({ grants: ['password'] }),
saveToken: function () {},
Expand Down Expand Up @@ -44,5 +44,32 @@ describe('TokenHandler', function () {
})
.catch(should.fail);
});

it('should call `model.getClient()` with no secret is provided (public client)', function () {
Comment thread
dsschiramm marked this conversation as resolved.
Outdated
const model = Model.from({
getClient: sinon.stub().returns({ grants: ['authorization_code'], type: 'public' }),
saveToken: function () {},
});
const handler = new TokenHandler({
accessTokenLifetime: 120,
model: model,
refreshTokenLifetime: 120,
});
const request = new Request({
body: { client_id: 'foo', grant_type: 'authorization_code' },
headers: {},
method: {},
query: {},
});

return handler
.getClient(request)
.then(function () {
model.getClient.callCount.should.equal(1);
model.getClient.firstCall.args[0].should.equal('foo');
should.equal(model.getClient.firstCall.args[1], null);
})
.catch(should.fail);
});
});
});
Loading