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');
}