Magento2 集成 Oauth 错误 - 验证随机数时发生错误
Magento2 Integration Oauth error - An error occurred validating the nonce
我正在尝试激活 Magento2,版本 2.4.4,与 expressjs 后端集成。
正在命中回调 url 并且数据正在存储在数据库中。然后点击身份url,弹出待集成应用登录弹窗,用户登录。
按照 https://devdocs.magento.com/guides/v2.4/get-started/authentication/gs-authentication-oauth.html#pre-auth-token 中定义的 oauth 流程向 /oauth/token/request
发出 POST 请求,我收到以下错误 -
oauth_problem=An+error+occurred+validating+the+nonce
我无法弄清楚这个错误的来源,请帮我解决这个问题,因为我已经坚持了很多天了。
以下是为 header 授权和 post body -
计算的值之一
Authorization: 'OAuth oauth_consumer_key=kxw5v6vwr4rm77cn2pxmqxdzdhhkor58, oauth_nonce=Fi9KRqgAmSX7sf32YpCTdPQ15FIY-LyY, oauth_signature=OTUzNWU4ZDViMzljZmM1NTM2MDNiMGQxOTUyMmRmMGRiMjdkZDZmNzY5ZTIxZTZkNGM1MzMzMmRkN2U5ZjcxNQ%3D%3D, oauth_signature_method=HMAC-SHA256, oauth_timestamp=1652694701394, oauth_version=1.0'
POST BODY -
{
oauth_consumer_key: 'kxw5v6vwr4rm77cn2pxmqxdzdhhkor58',
oauth_nonce: 'Fi9KRqgAmSX7sf32YpCTdPQ15FIY-LyY',
oauth_signature: 'OTUzNWU4ZDViMzljZmM1NTM2MDNiMGQxOTUyMmRmMGRiMjdkZDZmNzY5ZTIxZTZkNGM1MzMzMmRkN2U5ZjcxNQ%3D%3D',
oauth_signature_method: 'HMAC-SHA256',
oauth_timestamp: '1652694701394',
oauth_version: '1.0'
}
以下是回调url路由代码-
router.post('/magento-integration/callback', callbackHandler);
async function callbackHandler(req, res) {
const [{store_base_url, oauth_verifier, oauth_consumer_key, oauth_consumer_secret}] = [req.body];
try {
await saveOAuthCredentials({
store_base_url,
oauth_verifier,
oauth_consumer_key,
oauth_consumer_secret
});
return ApiResponse(res, 200);
} catch (err) {
// TODO: check err and set precise value of response status code and err msg
console.error(err.message)
return ApiResponse(res, 500, {message: err});
}
}
以下是身份控制器的代码url路由-
async function appLogin(req, res) {
// code to validate user
// ......
// Magento2 OAuth token exchange initiation
// Magento2 initiates the token exchange process by requesting the /login endpoint and sends
// url encoded query string params oauth_consumer_key and success_call_back which the front end sends in
// the body, against key queryParams, of the request it makes to /appLogin endpoint of sx-sellerapi.
const {oauth_consumer_key, success_call_back} = req.body.queryParams req.body.queryParams : [{}];
if(oauth_consumer_key && success_call_back){
try{
await runMagentoOAuthKeyX(sellerInfo.id, oauth_consumer_key);
res.redirect(success_call_back);
return;
} catch(err) {
return ApiResponse(res, 400, {message: err})
}
}
// rest of the code for usual login
}
runMagentoOAuthKeyX
代码
async function runMagentoOAuthKeyX(sellerId, oauthConsumerKey) {
try {
const oauthCred = await magentoModel.checkOAuthConsumerKeyExists(oauthConsumerKey, sellerId);
// isNonEmptyObject checks if arg passed is of type Object and has keys
if (isNonEmptyObject(oauthCred)) {
oauthCred.oauth_consumer_key = oauthConsumerKey;
oauthCred.url = `${oauthCred.store_base_url}${OAUTH_TOKEN_ENDPOINTS.request}`;
let requestTokenData;
try{
requestTokenData = await getToken(oauthCred, OAUTH_TOKEN_TYPE.requestToken);
} catch(err){
throw err
}
return Promise.all([
magentoModel.updateOAuthCred(oauthConsumerKey, requestTokenData, OAUTH_TOKEN_TYPE.requestToken),
getToken({...oauthCred, ...requestTokenData,
...{url: `${oauthCred.store_base_url}${OAUTH_TOKEN_ENDPOINTS.access}`}}, OAUTH_TOKEN_TYPE.accessToken)
])
.then(async ([_, accessTokenData]) =>
magentoModel.updateOAuthCred(oauthConsumerKey, accessTokenData, OAUTH_TOKEN_TYPE.accessToken)
)
.catch(err => {
throw err;
});
} else {
throw new Error(`OAuthConsumer key passed is unknown ${oauthConsumerKey}`);
}
} catch (err) {
// TODO: add logging
throw err;
}
getToken()
代码
async function getToken(tokenData, tokenType) {
const {url} = tokenData
const [authHeader, body] = await getAuthHeaderAndBody(tokenData, tokenType);
return axios.post(
url,
body,
{
headers: {
Authorization: authHeader
}
})
.catch(err => {
console.error(err.response.data);
throw err;
});
}
getAuthHeaderAndBody
代码
async function getAuthHeaderAndBody(tokenData, tokenType) {
const oauth_nonce = await genOAuthNonce();
const oauth_timestamp = Date.now();
const {
oauth_consumer_key,
oauth_consumer_secret,
oauth_signature_method,
url,
oauth_token,
oauth_token_secret,
oauth_verifier
} = tokenData;
const tokenList = ['access', 'webAPI'];
const oauthSignature = genOAuthSignature(url, {
oauth_consumer_key,
oauth_consumer_secret,
oauth_signature_method,
oauth_nonce,
oauth_timestamp,
oauth_version: OAUTH_VERSION,
oauth_token: tokenList.includes(tokenType) ? oauth_token : null,
oauth_token_secret: tokenList.includes(tokenType) ? oauth_token_secret : null,
oauth_verifier: OAUTH_TOKEN_TYPE.accessToken === tokenType ? oauth_verifier : null
});
const validParams = Object.entries({
oauth_consumer_key,
oauth_signature_method,
oauth_signature: oauthSignature,
oauth_nonce,
oauth_timestamp,
oauth_version: OAUTH_VERSION,
oauth_token: tokenList.includes(tokenType) ? oauth_token : null,
oauth_verifier: OAUTH_TOKEN_TYPE.accessToken == tokenType ? oauth_verifier : null
})
.filter(([_, val]) => val !== null)
.sort((a, b) => a[0] < b[0] ? -1 : 0);
const authHeaderValue = validParams
.map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
.join(', ');
const authHeaderStart = [OAUTH_TOKEN_TYPE.requestToken, OAUTH_TOKEN_TYPE.accessToken].includes(tokenType) ? 'OAuth' : 'Bearer';
const authHeader = `${authHeaderStart} ${authHeaderValue}`;
return [authHeader, Object.fromEntries(validParams)];
}
代码 genOAuthNonce
-
async function genOAuthNonce() {
const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._~';
const buff = Buffer.alloc(32);
const result = [];
return new Promise((resolve, reject) => crypto.randomFill(buff, (err, buff) => {
if(err){
reject(err);
}
buff.forEach(c => result.push(charset[c % charset.length]));
resolve(result.join(''));
}));
}
genOAuthSignature
代码
function genOAuthSignature(baseUrl, params, method = 'POST') {
const keysNotInSignature = ['oauth_consumer_secret', 'oauth_token_secret'];
const signatureString = Object.entries(params)
.filter(([key, val]) => val
!= null && !keysNotInSignature.includes(key))
.sort((item1, item2) => item1[0] < item2[0 ] ? -1 : 0)
.map(([key, val]) => `${key}=${val}`)
.join(AUTH_HEADER_DELIMITER);
const baseString = [
encodeURIComponent(method.toUpperCase()),
encodeURIComponent(baseUrl),
encodeURIComponent(signatureString)
].join(AUTH_HEADER_DELIMITER);
const {oauth_consumer_secret, oauth_token_secret} = params;
let signKey = `${encodeURIComponent(oauth_consumer_secret)}${AUTH_HEADER_DELIMITER}`
signKey += oauth_token_secret ? `${encodeURIComponent(oauth_token_secret)}` : '';
const hmac = createHmac('sha256', signKey);
return Buffer.from(hmac.update(baseString).digest('hex')).toString('base64');
}
发现了代码中无效 Nonce 的错误。问题出在时间戳上,因为我使用 Date.now() 其中 returns UTC 时间戳以毫秒为单位,而 magento2 oauth 要求它以秒为单位。还发现并修复了评估 oauth 令牌交换签名时的错误。
函数中getAuthHeaderAndBody
-
async function getAuthHeaderAndBody(tokenData, tokenType) {
const oauth_nonce = await genOAuthNonce();
// changed below from Date.now() as timestamp must be in seconds.
const oauth_timestamp = parseInt(Date.now() / 1000);
// ... rest of the code
}
在genOAuthSignature
function genOAuthSignature(baseUrl, params, method = 'POST') {
// preceding code
// last line is changed by looking at Magento2 code for validating the signature
return createHmac('sha256', signKey)
.update(baseString, 'binary')
.digest()
.toString('base64');
}
我正在尝试激活 Magento2,版本 2.4.4,与 expressjs 后端集成。 正在命中回调 url 并且数据正在存储在数据库中。然后点击身份url,弹出待集成应用登录弹窗,用户登录。
按照 https://devdocs.magento.com/guides/v2.4/get-started/authentication/gs-authentication-oauth.html#pre-auth-token 中定义的 oauth 流程向 /oauth/token/request
发出 POST 请求,我收到以下错误 -
oauth_problem=An+error+occurred+validating+the+nonce
我无法弄清楚这个错误的来源,请帮我解决这个问题,因为我已经坚持了很多天了。
以下是为 header 授权和 post body -
计算的值之一Authorization: 'OAuth oauth_consumer_key=kxw5v6vwr4rm77cn2pxmqxdzdhhkor58, oauth_nonce=Fi9KRqgAmSX7sf32YpCTdPQ15FIY-LyY, oauth_signature=OTUzNWU4ZDViMzljZmM1NTM2MDNiMGQxOTUyMmRmMGRiMjdkZDZmNzY5ZTIxZTZkNGM1MzMzMmRkN2U5ZjcxNQ%3D%3D, oauth_signature_method=HMAC-SHA256, oauth_timestamp=1652694701394, oauth_version=1.0'
POST BODY -
{
oauth_consumer_key: 'kxw5v6vwr4rm77cn2pxmqxdzdhhkor58',
oauth_nonce: 'Fi9KRqgAmSX7sf32YpCTdPQ15FIY-LyY',
oauth_signature: 'OTUzNWU4ZDViMzljZmM1NTM2MDNiMGQxOTUyMmRmMGRiMjdkZDZmNzY5ZTIxZTZkNGM1MzMzMmRkN2U5ZjcxNQ%3D%3D',
oauth_signature_method: 'HMAC-SHA256',
oauth_timestamp: '1652694701394',
oauth_version: '1.0'
}
以下是回调url路由代码-
router.post('/magento-integration/callback', callbackHandler);
async function callbackHandler(req, res) {
const [{store_base_url, oauth_verifier, oauth_consumer_key, oauth_consumer_secret}] = [req.body];
try {
await saveOAuthCredentials({
store_base_url,
oauth_verifier,
oauth_consumer_key,
oauth_consumer_secret
});
return ApiResponse(res, 200);
} catch (err) {
// TODO: check err and set precise value of response status code and err msg
console.error(err.message)
return ApiResponse(res, 500, {message: err});
}
}
以下是身份控制器的代码url路由-
async function appLogin(req, res) {
// code to validate user
// ......
// Magento2 OAuth token exchange initiation
// Magento2 initiates the token exchange process by requesting the /login endpoint and sends
// url encoded query string params oauth_consumer_key and success_call_back which the front end sends in
// the body, against key queryParams, of the request it makes to /appLogin endpoint of sx-sellerapi.
const {oauth_consumer_key, success_call_back} = req.body.queryParams req.body.queryParams : [{}];
if(oauth_consumer_key && success_call_back){
try{
await runMagentoOAuthKeyX(sellerInfo.id, oauth_consumer_key);
res.redirect(success_call_back);
return;
} catch(err) {
return ApiResponse(res, 400, {message: err})
}
}
// rest of the code for usual login
}
runMagentoOAuthKeyX
async function runMagentoOAuthKeyX(sellerId, oauthConsumerKey) {
try {
const oauthCred = await magentoModel.checkOAuthConsumerKeyExists(oauthConsumerKey, sellerId);
// isNonEmptyObject checks if arg passed is of type Object and has keys
if (isNonEmptyObject(oauthCred)) {
oauthCred.oauth_consumer_key = oauthConsumerKey;
oauthCred.url = `${oauthCred.store_base_url}${OAUTH_TOKEN_ENDPOINTS.request}`;
let requestTokenData;
try{
requestTokenData = await getToken(oauthCred, OAUTH_TOKEN_TYPE.requestToken);
} catch(err){
throw err
}
return Promise.all([
magentoModel.updateOAuthCred(oauthConsumerKey, requestTokenData, OAUTH_TOKEN_TYPE.requestToken),
getToken({...oauthCred, ...requestTokenData,
...{url: `${oauthCred.store_base_url}${OAUTH_TOKEN_ENDPOINTS.access}`}}, OAUTH_TOKEN_TYPE.accessToken)
])
.then(async ([_, accessTokenData]) =>
magentoModel.updateOAuthCred(oauthConsumerKey, accessTokenData, OAUTH_TOKEN_TYPE.accessToken)
)
.catch(err => {
throw err;
});
} else {
throw new Error(`OAuthConsumer key passed is unknown ${oauthConsumerKey}`);
}
} catch (err) {
// TODO: add logging
throw err;
}
getToken()
async function getToken(tokenData, tokenType) {
const {url} = tokenData
const [authHeader, body] = await getAuthHeaderAndBody(tokenData, tokenType);
return axios.post(
url,
body,
{
headers: {
Authorization: authHeader
}
})
.catch(err => {
console.error(err.response.data);
throw err;
});
}
getAuthHeaderAndBody
async function getAuthHeaderAndBody(tokenData, tokenType) {
const oauth_nonce = await genOAuthNonce();
const oauth_timestamp = Date.now();
const {
oauth_consumer_key,
oauth_consumer_secret,
oauth_signature_method,
url,
oauth_token,
oauth_token_secret,
oauth_verifier
} = tokenData;
const tokenList = ['access', 'webAPI'];
const oauthSignature = genOAuthSignature(url, {
oauth_consumer_key,
oauth_consumer_secret,
oauth_signature_method,
oauth_nonce,
oauth_timestamp,
oauth_version: OAUTH_VERSION,
oauth_token: tokenList.includes(tokenType) ? oauth_token : null,
oauth_token_secret: tokenList.includes(tokenType) ? oauth_token_secret : null,
oauth_verifier: OAUTH_TOKEN_TYPE.accessToken === tokenType ? oauth_verifier : null
});
const validParams = Object.entries({
oauth_consumer_key,
oauth_signature_method,
oauth_signature: oauthSignature,
oauth_nonce,
oauth_timestamp,
oauth_version: OAUTH_VERSION,
oauth_token: tokenList.includes(tokenType) ? oauth_token : null,
oauth_verifier: OAUTH_TOKEN_TYPE.accessToken == tokenType ? oauth_verifier : null
})
.filter(([_, val]) => val !== null)
.sort((a, b) => a[0] < b[0] ? -1 : 0);
const authHeaderValue = validParams
.map(([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`)
.join(', ');
const authHeaderStart = [OAUTH_TOKEN_TYPE.requestToken, OAUTH_TOKEN_TYPE.accessToken].includes(tokenType) ? 'OAuth' : 'Bearer';
const authHeader = `${authHeaderStart} ${authHeaderValue}`;
return [authHeader, Object.fromEntries(validParams)];
}
代码 genOAuthNonce
-
async function genOAuthNonce() {
const charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._~';
const buff = Buffer.alloc(32);
const result = [];
return new Promise((resolve, reject) => crypto.randomFill(buff, (err, buff) => {
if(err){
reject(err);
}
buff.forEach(c => result.push(charset[c % charset.length]));
resolve(result.join(''));
}));
}
genOAuthSignature
function genOAuthSignature(baseUrl, params, method = 'POST') {
const keysNotInSignature = ['oauth_consumer_secret', 'oauth_token_secret'];
const signatureString = Object.entries(params)
.filter(([key, val]) => val
!= null && !keysNotInSignature.includes(key))
.sort((item1, item2) => item1[0] < item2[0 ] ? -1 : 0)
.map(([key, val]) => `${key}=${val}`)
.join(AUTH_HEADER_DELIMITER);
const baseString = [
encodeURIComponent(method.toUpperCase()),
encodeURIComponent(baseUrl),
encodeURIComponent(signatureString)
].join(AUTH_HEADER_DELIMITER);
const {oauth_consumer_secret, oauth_token_secret} = params;
let signKey = `${encodeURIComponent(oauth_consumer_secret)}${AUTH_HEADER_DELIMITER}`
signKey += oauth_token_secret ? `${encodeURIComponent(oauth_token_secret)}` : '';
const hmac = createHmac('sha256', signKey);
return Buffer.from(hmac.update(baseString).digest('hex')).toString('base64');
}
发现了代码中无效 Nonce 的错误。问题出在时间戳上,因为我使用 Date.now() 其中 returns UTC 时间戳以毫秒为单位,而 magento2 oauth 要求它以秒为单位。还发现并修复了评估 oauth 令牌交换签名时的错误。
函数中getAuthHeaderAndBody
-
async function getAuthHeaderAndBody(tokenData, tokenType) {
const oauth_nonce = await genOAuthNonce();
// changed below from Date.now() as timestamp must be in seconds.
const oauth_timestamp = parseInt(Date.now() / 1000);
// ... rest of the code
}
在genOAuthSignature
function genOAuthSignature(baseUrl, params, method = 'POST') {
// preceding code
// last line is changed by looking at Magento2 code for validating the signature
return createHmac('sha256', signKey)
.update(baseString, 'binary')
.digest()
.toString('base64');
}