mirror of
https://github.com/mozilla/fxa.git
synced 2025-12-13 20:36:41 +01:00
140 lines
4.6 KiB
JavaScript
140 lines
4.6 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const crypto = require('crypto');
|
|
const buf = (v) => (Buffer.isBuffer(v) ? v : Buffer.from(v, 'hex'));
|
|
const Joi = require('joi');
|
|
|
|
const { OauthError } = require('@fxa/accounts/errors');
|
|
const validators = require('./validators');
|
|
const db = require('./db');
|
|
const encrypt = require('fxa-shared/auth/encrypt');
|
|
|
|
// Client credentials can be provided in either the Authorization header
|
|
// or the request body, but not both.
|
|
// These are some re-useable validators to assert that requirement.
|
|
module.exports.clientAuthValidators = {
|
|
headers: Joi.object({
|
|
authorization: Joi.string().regex(validators.BASIC_AUTH_HEADER).optional(),
|
|
}).options({ allowUnknown: true, stripUnknown: false }),
|
|
|
|
// The use of `$headers` here is Joi syntax for a "context reference"
|
|
// as described at https://hapi.dev/family/joi/?v=16.1.4#refkey-options.
|
|
// Hapi provides the headers as part of the context when validating request payload,
|
|
// as noted in https://github.com/hapijs/hapi/blob/master/API.md#-routeoptionsresponseschema.
|
|
clientId: validators.clientId.when('$headers.authorization', {
|
|
is: Joi.string().required(),
|
|
then: Joi.forbidden(),
|
|
}),
|
|
|
|
clientSecret: validators.clientSecret.when('$headers.authorization', {
|
|
is: Joi.string().required(),
|
|
then: Joi.forbidden(),
|
|
}),
|
|
};
|
|
|
|
/**
|
|
* Extract and normalize client credentials from a request.
|
|
* Clients may provide credentials in either the Authorization header
|
|
* or the request body, but not both.
|
|
*
|
|
* @param {Object} headers the headers from the request
|
|
* @param {Object} params the payload from the request
|
|
* @returns {Object} credentials
|
|
* @param {String} client_id
|
|
* @param {String} [client_secret]
|
|
*/
|
|
module.exports.getClientCredentials = function getClientCredentials(
|
|
headers,
|
|
params
|
|
) {
|
|
const creds = {};
|
|
creds.client_id = params.client_id;
|
|
if (params.client_secret) {
|
|
creds.client_secret = params.client_secret;
|
|
}
|
|
|
|
// Clients are allowed to provide credentials in either
|
|
// the Authorization header or request body, but not both.
|
|
if (headers.authorization) {
|
|
const authzMatch = validators.BASIC_AUTH_HEADER.exec(headers.authorization);
|
|
const err = OauthError.invalidRequestParameter({
|
|
keys: ['authorization'],
|
|
});
|
|
if (!authzMatch || creds.client_id || creds.client_secret) {
|
|
throw err;
|
|
}
|
|
const [clientId, clientSecret, ...rest] = Buffer.from(
|
|
authzMatch[1],
|
|
'base64'
|
|
)
|
|
.toString()
|
|
.split(':');
|
|
if (rest.length !== 0) {
|
|
throw err;
|
|
}
|
|
creds.client_id = Joi.attempt(clientId, validators.clientId, err);
|
|
creds.client_secret = Joi.attempt(
|
|
clientSecret,
|
|
validators.clientSecret,
|
|
err
|
|
);
|
|
}
|
|
|
|
return creds;
|
|
};
|
|
|
|
/**
|
|
* Authenticate a request made by an OAuth client, using credentials from
|
|
* either the Authorization header or request body parameters.
|
|
*
|
|
* @param {Object} headers the headers from the request
|
|
* @param {Object} params the payload from the request
|
|
* @returns {Promise} resolves with info about the client, or
|
|
* rejects if invalid credentials were provided
|
|
*/
|
|
module.exports.authenticateClient = async function authenticateClient(
|
|
headers,
|
|
params
|
|
) {
|
|
const creds = exports.getClientCredentials(headers, params);
|
|
|
|
const client = await exports.getClientById(creds.client_id);
|
|
|
|
// Public clients can't be authenticated in any useful way,
|
|
// and should never submit a client_secret.
|
|
if (client.publicClient) {
|
|
if (creds.client_secret) {
|
|
throw OauthError.invalidRequestParameter({ keys: ['client_secret'] });
|
|
}
|
|
return client;
|
|
}
|
|
|
|
// Check client_secret against both current and previous stored secrets,
|
|
// to allow for seamless rotation of the secret.
|
|
if (!creds.client_secret) {
|
|
throw OauthError.invalidRequestParameter({ keys: ['client_secret'] });
|
|
}
|
|
const submitted = encrypt.hash(buf(creds.client_secret));
|
|
const stored = client.hashedSecret;
|
|
if (crypto.timingSafeEqual(submitted, stored)) {
|
|
return client;
|
|
}
|
|
const storedPrevious = client.hashedSecretPrevious;
|
|
if (storedPrevious) {
|
|
if (crypto.timingSafeEqual(submitted, storedPrevious)) {
|
|
return client;
|
|
}
|
|
}
|
|
throw OauthError.incorrectSecret(client.id);
|
|
};
|
|
|
|
module.exports.getClientById = async function getClientById(clientId) {
|
|
const client = await db.getClient(buf(clientId));
|
|
if (!client) {
|
|
throw OauthError.unknownClient(clientId);
|
|
}
|
|
return client;
|
|
};
|