How to createContact with xero-node custom connection - xero-api

Using Xero-node with a custom connection, the createContacts endpoint has been authorised for my app but any attempt to create a contact produces the same error:
"ErrorNumber": 17,
"Type": "NoDataProcessedException",
"Message": "No data has been processed for this endpoint. This endpoint is expecting Contact data to be specified in the request body."
Here's the code used to call xero-node after capturing tokens etc.:
let response
const contacts = [{EmailAddress:"any#email.com",Name:"the name"}]
try {
response = await xero.accountingApi.createContacts('',contacts,true);
console.log(response.body || response.response.statusCode)
} catch (err) {
const error = JSON.stringify(err.response.body, null, 2)
console.log(`Status Code: ${err.response.statusCode} => ${error}`);
return {error:"Unable to create Xero contact"}
}
Anyone know how to make this work?

Related

'Unexpected end of JSON input' error while working with Shopify webhooks

I've created an app and try to register for webhooks, and then fetch the list of all webhooks.
I use this code for this (/server/middleware/auth.js):
const webhook = new Webhook({ session: session });
webhook.topic = "products/update";
webhook.address = "https://api.service.co/items/update";
webhook.format = "json";
console.log("registering products/update");
try {
await webhook.save({
update: true,
});
} catch (error) {
console.log(error);
}
const webhookSecond = new Webhook({ session: session });
webhookSecond.topic = "products/create";
webhookSecond.address = "https://api.service.co/items/webhooks";
webhookSecond.format = "json";
console.log("registering products/create");
try {
await webhookSecond.save({
update: true,
});
} catch (error) {
console.log(error);
}
console.log("getting all webhooks");
try {
let webhooks = await Webhook.all({
session: session,
});
console.log(webhooks);
} catch (error) {
console.log(error);
}
Everything works fine for a development store. However, when I try to launch this script on a third-party customer store, then I get this error:
HttpRequestError: Failed to make Shopify HTTP request: FetchError: invalid json response body at https://shopname.myshopify.com/admin/api/2022-04/webhooks.json reason: Unexpected end of JSON input
The app permissions/scopes are: read_checkouts, read_orders, read_inventory, read_products, read_customers
I got this error 3 times, even for Webhook.all.
Could you please tell me what can cause this error, and how could it be fixed?
This error was caused by the lack of access provided by the owner of the store to my collaborator developer account. Manage settings access was required.

How to debug invalid_grant during authorization code exchange?

My application is using OAuth to access the Youtube Data API. My OAuth callback is written in node and uses the OAuth2Client class from the "googleapis" npm package to exchange the authorization code for the access and refresh tokens.
Everything was working fine up to last week until suddenly I started getting the "invalid_grant" response during the authorization code exchange. I have tried everything to resolve this and am running out of ideas. My callback executes as a cloud function so I don't think that it would be out of sync with NTP.
My OAuth consent screen is in "Testing" mode and my email address is included in the test users. The odd thing is that even though the authorization code exchange fails, my Google account's "Third-party apps with account access" section lists my application as if the handshake succeeded.
Here is my code for generating the auth URL and exchanging the authorization code. The invalid_grant occurs during the call to "oauth2.getToken"
async startFlow(scopes: string[], state: string): Promise<AuthFlow> {
const codes = await oauth2.generateCodeVerifierAsync();
const href = oauth2.generateAuthUrl({
scope: scopes,
state,
access_type: 'offline',
include_granted_scopes: true,
prompt: 'consent',
code_challenge_method: CodeChallengeMethod.S256,
code_challenge: codes.codeChallenge
});
return { href, code_verifier: codes.codeVerifier };
}
async finishFlow(code: string, verifier: string): Promise<Tokens> {
const tokens = await oauth2.getToken({ code, codeVerifier: verifier })
return {
refresh_token: tokens.tokens.refresh_token!,
access_token: tokens.tokens.access_token!,
expires_in: tokens.tokens.expiry_date!,
token_type: 'Bearer',
scopes: tokens.tokens.scope!.split(' ')
};
}
"oauth2" is an instance of OAuth2Client from "google-auth-library". I initialize it here:
export const oauth2 = new google.auth.OAuth2({
clientId: YT_CLIENT_ID,
clientSecret: YT_CLIENT_SECRET,
redirectUri: `${APP_URI}/oauth`
});
Looking at the logs, the only out of the ordinary thing I notice is that the application/x-www-form-urlencoded body looks slightly different than the example https://developers.google.com/identity/protocols/oauth2/web-server#exchange-authorization-code
The POST request to "https://oauth2.googleapis.com/token" ends up looking like this:
code=4%2F0AX4XfWiKHVnsavUH7en0TywjPJVRyJ9aGN-JR8CAAcAG7dT-THxyWQNcxd769nzaHLUb8Q&client_id=XXXXXXXXXX-XXXXXXXXXXXXXXX.apps.googleusercontent.com&client_secret=XXXXXX-XXXXXXXXXXXXXXX-XX_XXX&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth&grant_type=authorization_code&code_verifier=KjOBmr4D9ISLPSE4claEBWr3UN-bKdPHZa8BBcQvcmajfr9RhWrgt7G429PLEpsP7oGzFGnBICu3HgWaHPsLhMkGBuQ2GmHHiB4OpY2F0rJ06wkpCjV2cCTDdpfRY~Ej
Notice that the "/" characters are not percent-encoded in the official example, but they are in my requests. Could this actually be the issue? I don't see how the official google auth library would have an issue this large.
If you check the documentation on expiration of refresh tokens you will see
A Google Cloud Platform project with an OAuth consent screen configured for an external user type and a publishing status of "Testing" is issued a refresh token expiring in 7 days.
Set your project to production mode and your refresh tokens will last longer then a week.
library
IMO if your refresh token expires your library should be requesting access again. However the library was created before the change that a refresh token gets revoked. This isnt a true refresh token expired error message and the library isn't detecting that.
A post over on the issue forum for your library may encourage them to patch it so that it is more obvious of an error message.
At any rate you need to delete the stored tokens for that user which will request authorization again.
update If its not that.
ensure that your computer / server has the correct time
revoke the users access directly via their google account. Under third party apps with access ensure that your app isnt listed if it is remove it.
Make sure that you are using the correct client type from google developer console.
Google node.js sample
var fs = require('fs');
var readline = require('readline');
var {google} = require('googleapis');
var OAuth2 = google.auth.OAuth2;
// If modifying these scopes, delete your previously saved credentials
// at ~/.credentials/youtube-nodejs-quickstart.json
var SCOPES = ['https://www.googleapis.com/auth/youtube.readonly'];
var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
process.env.USERPROFILE) + '/.credentials/';
var TOKEN_PATH = TOKEN_DIR + 'youtube-nodejs-quickstart.json';
// Load client secrets from a local file.
fs.readFile('client_secret.json', function processClientSecrets(err, content) {
if (err) {
console.log('Error loading client secret file: ' + err);
return;
}
// Authorize a client with the loaded credentials, then call the YouTube API.
authorize(JSON.parse(content), getChannel);
});
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
*
* #param {Object} credentials The authorization client credentials.
* #param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
var clientSecret = credentials.installed.client_secret;
var clientId = credentials.installed.client_id;
var redirectUrl = credentials.installed.redirect_uris[0];
var oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, function(err, token) {
if (err) {
getNewToken(oauth2Client, callback);
} else {
oauth2Client.credentials = JSON.parse(token);
callback(oauth2Client);
}
});
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
*
* #param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
* #param {getEventsCallback} callback The callback to call with the authorized
* client.
*/
function getNewToken(oauth2Client, callback) {
var authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES
});
console.log('Authorize this app by visiting this url: ', authUrl);
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Enter the code from that page here: ', function(code) {
rl.close();
oauth2Client.getToken(code, function(err, token) {
if (err) {
console.log('Error while trying to retrieve access token', err);
return;
}
oauth2Client.credentials = token;
storeToken(token);
callback(oauth2Client);
});
});
}
/**
* Store token to disk be used in later program executions.
*
* #param {Object} token The token to store to disk.
*/
function storeToken(token) {
try {
fs.mkdirSync(TOKEN_DIR);
} catch (err) {
if (err.code != 'EEXIST') {
throw err;
}
}
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) throw err;
console.log('Token stored to ' + TOKEN_PATH);
});
}
/**
* Lists the names and IDs of up to 10 files.
*
* #param {google.auth.OAuth2} auth An authorized OAuth2 client.
*/
function getChannel(auth) {
var service = google.youtube('v3');
service.channels.list({
auth: auth,
part: 'snippet,contentDetails,statistics',
forUsername: 'GoogleDevelopers'
}, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
return;
}
var channels = response.data.items;
if (channels.length == 0) {
console.log('No channel found.');
} else {
console.log('This channel\'s ID is %s. Its title is \'%s\', and ' +
'it has %s views.',
channels[0].id,
channels[0].snippet.title,
channels[0].statistics.viewCount);
}
});
}
I've discovered my problem. Another part of my code was throwing an error while the authorization request was being sent (some weird async code). This caused the request to bail halfway through and send only a partial body. This resulted in the invalid_grant appearing in my logs which misled me to believe this was the root cause of my problem.
I fixed the other piece of my code and now the authorization request succeeds without issue.

How to properly call and implement ConnectyCube's External auth via Custom Identity Provider

I'm trying to implement Custom Identity Provider of ConnectyCube.
The instructions are:
I am now trying to implement the last step:
POST https://api.connectycube.com/login
login=IP_user_token
I try to do the above by the following code:
const login = {
login: token,
};
try {
const {data} = await axios.post(
`https://api.connectycube.com/login`,
login,
);
console.log('data', data);
} catch (err) {
console.log('err while calling api.connectycube.com/login err', err);
}
When I do that though I get the following 403 error:
[Error: Request failed with status code 403]
Am I POSTing incorrectly?
How to fix?

How to customize the authorization error produced by OpenIddict?

I'm using OpenIddict for auth in a .NET Core 2 API. Client side I'm relying on any API errors to follow a custom scheme. However, when e.g. a refresh token has been outdated, I can't seem to find out how to customize the error sent back.
The /token endpoint is never reached, so the error is not under "my control".
The result of the request is a status code 400, with the following JSON:
{"error":"invalid_grant","error_description":"The specified refresh token is no longer valid."}
I've tried to use a custom middleware to catch all status codes (which it does), but the result is returned before the execution of my custom middleware has completed.
How can I properly customize the error or intercept to change it? Thanks!
You can use OpenIddict's event model to customize the token response payloads before they are written to the response stream. Here's an example:
MyApplyTokenResponseHandler.cs
public class MyApplyTokenResponseHandler : IOpenIddictServerEventHandler<ApplyTokenResponseContext>
{
public ValueTask HandleAsync(ApplyTokenResponseContext context)
{
var response = context.Response;
if (string.Equals(response.Error, OpenIddictConstants.Errors.InvalidGrant, StringComparison.Ordinal) &&
!string.IsNullOrEmpty(response.ErrorDescription))
{
response.ErrorDescription = "Your customized error";
}
return default;
}
}
Startup.cs
services.AddOpenIddict()
.AddCore(options =>
{
// ...
})
.AddServer(options =>
{
// ...
options.AddEventHandler<ApplyTokenResponseContext>(builder =>
builder.UseSingletonHandler<MyApplyTokenResponseHandler>());
})
.AddValidation();
The /token endpoint is never reached, so the error is not under "my control".
In fact ,the /token is reached, and the parameter of grant_type equals refresh_token. But the rejection logic when refresh token expired is not processed by us. It is some kind of "hardcoded" in source code :
if (token == null)
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
return;
}
if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType())
{
if (!await TryRedeemTokenAsync(token))
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
return;
}
}
The context.Reject here comes from the assembly AspNet.Security.OpenIdConnect.Server.
For more details, see source code on GitHub .
I've tried to use a custom middleware to catch all status codes (which it does), but the result is returned before the execution of my custom middleware has completed.
I've tried and I'm pretty sure we can use a custom middleware to catch all status codes. The key point is to detect the status code after the next() invocation:
app.Use(async(context , next )=>{
// passby all other end points
if(! context.Request.Path.StartsWithSegments("/connect/token")){
await next();
return;
}
// since we might want to detect the Response.Body, I add some stream here .
// if you only want to detect the status code , there's no need to use these streams
Stream originalStream = context.Response.Body;
var hijackedStream = new MemoryStream();
context.Response.Body = hijackedStream;
hijackedStream.Seek(0,SeekOrigin.Begin);
await next();
// if status code not 400 , pass by
if(context.Response.StatusCode != 400){
await CopyStreamToResponseBody(context,hijackedStream,originalStream);
return;
}
// read and custom the stream
hijackedStream.Seek(0,SeekOrigin.Begin);
using (StreamReader sr = new StreamReader(hijackedStream))
{
var raw= sr.ReadToEnd();
if(raw.Contains("The specified refresh token is no longer valid.")){
// custom your own response
context.Response.StatusCode = 401;
// ...
//context.Response.Body = ... /
}else{
await CopyStreamToResponseBody(context,hijackedStream,originalStream);
}
}
});
// helper to make the copy easy
private async Task CopyStreamToResponseBody(HttpContext context,Stream newStream, Stream originalStream){
newStream.Seek(0,SeekOrigin.Begin);
await newStream.CopyToAsync(originalStream);
context.Response.ContentLength =originalStream.Length;
context.Response.Body = originalStream;
}

Show API error in notification

I'm missing something basic in the docs. When I get an API validation error, I'm returning a status code and message. It appears that React-Admin is translating the status code to a generic HTTP error code.
My error response.
{"error":
{"statusCode":422,
"name":"Error",
"message":"User with same first and last name already on team."}
}
When my API response with that response, I'm seeing "Unprocessable Entity" in the notification box. I'm using SimpleForm.
I know the status code is being recognized because I've changed the 422 and it shows the corresponding HTTP error description.
In the docs it says to throw and error in your data provider. I've moved that the Simple Rest data provider into my project and have tried throwing errors are various places, but nothing changes on the client.
https://marmelab.com/react-admin/DataProviders.html#error-format
If you have customized error from your API, I'd appreciated any hints you can give. Thx.
Here is the actual error processing:
When a fetch is triggered (usually coming from the data provider), if an error happen, it is caught and transformed into an HttpError and re-thrown (source)
In the process, the HTTP Error message becomes either the json.message or the response statusText. It's here that a 422 HTTP Error becomes Unprocessable Entity (source)
Then the error is caught again at a higher level to be transformed into a redux action. (source)
Finally, the error is transformed into a notification containing the error message.
So, in order to customize your error message, you can easily do that from your custom provider by catching the error in the first place, customizing the error message, and send it again:
const dataProvider = (type, resource, params) => new Promise((resolve, reject) => {
if (type === 'GET_LIST' && resource === 'posts') {
return fetch(...args)
.then(res => res.json())
.then(json => {
if (json.error) {
// The notification will show what's in { error: "message" }
reject(new Error(json.error.message));
return;
}
resolve(json);
});
}
// ...
});
in Backend, I structure the response as
res.json({status:400,message:"Email Address is invalid!"})
In Client side, modify the convertHTTPResponse in dataprovider as:
const convertHTTPResponse = (response, type, resource, params) => {
const { headers, json } = response;
switch (type) {
case GET_LIST:
case GET_MANY_REFERENCE:
if(json.status === 200){
if (!headers.has('content-range')) {
throw new Error('The Content-Range header is missing in the HTTP Response.);
}
return {
data: json.docs,
total: parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
),
};
}else{
throw new Error(json.message)
}
default:
if(json.status === 200){
return { data: json.docs };
}else{
throw new Error(json.message)
}
}