Skip to content
Open
Show file tree
Hide file tree
Changes from all 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);

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 {
}
}

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 () {
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