Skip to content

Commit

Permalink
Use secretHash, deprecate passwordHash, use grant_types_supported.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Jan 27, 2025
1 parent 13e9c31 commit 7090519
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 43 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# bedrock-basic-authz-server ChangeLog

## 1.1.0 - 2025-01-dd

### Changed
- Use `secretHash` instead of `passwordHash` (now deprecated but still
available for use) in oauth2 client configuration.

## 1.0.0 - 2025-01-26

- See git history for changes.
6 changes: 3 additions & 3 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ cfg.authorization = {
// scopes that can be requested in the future; changing this DOES NOT
// alter existing access (for already issued tokens)
requestableScopes: ...,
// base64url-encoding of a SHA-256 of the client ID's password;
// security depends on passwords being sufficiently large (16 bytes or
// base64url-encoding of a SHA-256 of the client ID's secret;
// security depends on secrets being sufficiently large (16 bytes or
// more) random strings; this field should be populated using an
// appropriate cloud secret store in any deployment
passwordHash
secretHash
}
*/
},
Expand Down
70 changes: 39 additions & 31 deletions lib/http/oauth2.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ bedrock.events.on('bedrock.init', async () => {
config: {
issuer: baseUri,
jwks_uri: `${baseUri}${routes.jwks}`,
token_endpoint: `${baseUri}${routes.token}`
token_endpoint: `${baseUri}${routes.token}`,
grant_types_supported: ['client_credentials']
}
};

Expand Down Expand Up @@ -83,8 +84,7 @@ bedrock.events.on('bedrock.init', async () => {

// build map of client_id => client from named clients
for(const clientName in clients) {
const client = clients[clientName];
_assertOAuth2Client({client});
const client = _importOAuth2Client({client: clients[clientName]});
CLIENT_MAP.set(client.id, client);
}
});
Expand Down Expand Up @@ -144,34 +144,17 @@ export async function checkAccessToken({req, getExpectedValues} = {}) {
});
}

function _assertOAuth2Client({client} = {}) {
// do not use assert on whole object to avoid logging client password
if(!(client && typeof client === 'object')) {
throw new TypeError(
'Invalid oauth2 client configuration; client is not an object.');
}
assert.string(client.id, 'client.id');
assert.arrayOfString(client.requestableScopes, 'client.requestableScopes');
if(!(typeof client.passwordHash === 'string' &&
Buffer.from(client.passwordHash, 'base64url').length === 32)) {
throw new TypeError(
'Invalid oauth2 client configuration; ' +
'"passwordHash" must be a base64url-encoded SHA-256 hash of the ' +
`client's sufficiently large, random password.`);
}
}

async function _assertOauth2ClientPassword({client, password}) {
// hash password for comparison (fast hash is used here which presumes
// passwords are large and random so no rainbow table can be built but
// the passwords won't be stored directly)
const passwordHash = await _sha256(password);
async function _assertOauth2ClientSecret({client, secret}) {
// hash secret for comparison (fast hash is used here which presumes
// secrets are large and random so no rainbow table can be built but
// the secrets won't be stored directly)
const secretHash = await _sha256(secret);

// ensure given password hash matches client record
// ensure given secret hash matches client record
if(!timingSafeEqual(
Buffer.from(client.passwordHash, 'base64url'), passwordHash)) {
Buffer.from(client.secretHash, 'base64url'), secretHash)) {
throw new BedrockError(
'Invalid OAuth2 client password.', {
'Invalid OAuth2 client secret.', {
name: 'NotAllowedError',
details: {
httpStatusCode: 403,
Expand All @@ -190,14 +173,14 @@ async function _checkBasicAuthorization({req}) {
// parse credentials
// see: https://datatracker.ietf.org/doc/html/rfc7617#section-2
const {
credentials: {userId: clientId, password}
credentials: {userId: clientId, password: secret}
} = getBasicAuthorizationCredentials({req});

// find matching client
const client = await _getOAuth2Client({clientId});

// assert password
await _assertOauth2ClientPassword({client, password});
// assert secret
await _assertOauth2ClientSecret({client, secret});

return {client};
} catch(cause) {
Expand Down Expand Up @@ -291,6 +274,31 @@ function _getRequestedScopes({client, request}) {
return scopes;
}

function _importOAuth2Client({client} = {}) {
// do not use assert on whole object to avoid logging client secret
if(!(client && typeof client === 'object')) {
throw new TypeError(
'Invalid oauth2 client configuration; client is not an object.');
}
const {id, requestableScopes} = client;
assert.string(id, 'client.id');
assert.arrayOfString(requestableScopes, 'client.requestableScopes');
const secretHash = client.secretHash ?? client.passwordHash;
if(!(typeof secretHash === 'string' &&
Buffer.from(secretHash, 'base64url').length === 32)) {
throw new TypeError(
'Invalid oauth2 client configuration; ' +
'"secretHash" (or deprecated "passwordHash") must be a ' +
'base64url-encoded SHA-256 hash of the ' +
`client's sufficiently large, random secret.`);
}
return {
id,
requestableScopes,
secretHash
};
}

async function _processAccessTokenRequest({req}) {
// only "client_credentials" grant type is supported
// see: https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
Expand Down
10 changes: 5 additions & 5 deletions test/mocha/10-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ describe('http API', () => {
result = await helpers.requestOAuth2AccessToken({
url,
clientId: clients.authorizedClient.id,
password: clients.authorizedClient.id,
secret: clients.authorizedClient.id,
requestedScopes: [`read:${target}`]
});
} catch(e) {
Expand All @@ -282,7 +282,7 @@ describe('http API', () => {
result = await helpers.requestOAuth2AccessToken({
url,
clientId: clients.authorizedClient.id,
password: clients.authorizedClient.id,
secret: clients.authorizedClient.id,
requestedScopes: [`read:${target}`, `write:${target}`]
});
} catch(e) {
Expand All @@ -299,7 +299,7 @@ describe('http API', () => {
result = await helpers.requestOAuth2AccessToken({
url,
clientId: clients.authorizedClient.id,
password: clients.authorizedClient.id,
secret: clients.authorizedClient.id,
requestedScopes: [`read:/`]
});
} catch(e) {
Expand All @@ -317,7 +317,7 @@ describe('http API', () => {
result = await helpers.requestOAuth2AccessToken({
url,
clientId: clients.unauthorizedClient.id,
password: clients.unauthorizedClient.id,
secret: clients.unauthorizedClient.id,
requestedScopes: [`read:${target}`]
});
} catch(e) {
Expand All @@ -334,7 +334,7 @@ describe('http API', () => {
} = await helpers.requestOAuth2AccessToken({
url,
clientId: clients.authorizedClient.id,
password: clients.authorizedClient.id,
secret: clients.authorizedClient.id,
requestedScopes: [`read:${target}`]
});
let err;
Expand Down
4 changes: 2 additions & 2 deletions test/mocha/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ export async function doOAuth2Request({url, json, accessToken}) {
}

export async function requestOAuth2AccessToken({
url, clientId, password, requestedScopes
url, clientId, secret, requestedScopes
}) {
const body = new URLSearchParams({
grant_type: 'client_credentials',
scope: requestedScopes.join(' ')
});
const credentials = Buffer.from(`${clientId}:${password}`).toString('base64');
const credentials = Buffer.from(`${clientId}:${secret}`).toString('base64');
const headers = {
accept: 'application/json',
authorization: `Basic ${credentials}`
Expand Down
4 changes: 2 additions & 2 deletions test/test.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ clients.authorizedClient = {
'read:/test-authorize-request',
'write:/test-authorize-request'
],
passwordHash: 'qpMmqCHdQ0FkyVCF1Sfuprt4jKZ4p4Id1LhSLxmdmu8'
secretHash: 'qpMmqCHdQ0FkyVCF1Sfuprt4jKZ4p4Id1LhSLxmdmu8'
};
clients.unauthorizedClient = {
id: '5165774d-fadc-484b-8a78-d2b049721b52',
// no requestable scopes
requestableScopes: [],
// hash of `client_id`
passwordHash: 'JySRI3hb_DJ3rV4oUulOowEcLkRS4DCMdnfzJx57Z3g'
secretHash: 'JySRI3hb_DJ3rV4oUulOowEcLkRS4DCMdnfzJx57Z3g'
};

0 comments on commit 7090519

Please sign in to comment.