I am trying to activate Magento2, version 2.4.4, integration with expressjs backend. The callback url is being hit and the data is being stored in the db. Then upon hitting the identity url, the pop up of login for app to be integrated is opened and user logs in.
Following the oauth process as defined at https://devdocs.magento.com/guides/v2.4/get-started/authentication/gs-authentication-oauth.html#pre-auth-token on making the POST request to /oauth/token/request
I'm getting the following error -
oauth_problem=An+error+occurred+validating+the+nonce
I cannot figure out the source of this error, please help me fix this as I've been stuck at it since many days.
Following are one of the values calculated for the header Authorization and the 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'
}
Following is callback url route code -
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});
}
}
Following is the code for the controller of identity url route -
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
}
Code for 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;
}
Code for 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;
});
}
Code for 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)];
}
Code for 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(''));
}));
}
Code for 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');
}
Found the bugs in the code for invalid Nonce. The issue was with timestamp as I was using Date.now() which returns UTC timestamp in ms whereas magento2 oauth requires it to be in seconds. Also found and fixed the bug in evaluating the signature for oauth token exchange.
In function 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
}
In 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');
}