I am trying to build an app using zapier cli.
I am having a problem with authorizing the request .
I am using an api key and a client id.
When I try to use the same credentials in the UI
Its working perfect , however in the cli it gives an error code 403.
I have listed the code below . What could be the issue ?
//test
"use strict";
const should = require("should");
const zapier = require("zapier-platform-core");
const App = require("../index");
const appTester = zapier.createAppTester(App);
describe("custom authentication authentication", () => {
// Put your test TEST_USERNAME and TEST_PASSWORD in a .env file.
// The inject method will load them and make them available to use in your
// tests.
zapier.tools.env.inject();
it("should authenticate", (done) => {
const bundle = {
authData: {
api_key: process.env.API_KEY,
client_id: process.env.CLIENT_ID,
},
};
appTester(App.authentication, bundle)
.then((response) => {
should.exist(response);
done();
})
.catch(done);
});
});
//authentication.js
"use strict";
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = currentDate.getMonth() + 1;
const day = currentDate.getDate();
const authentication = (z, bundle) => {
const options = {
url: "url",
method: "GET",
headers: {
ContentType: "application/json",
Accept: "application/json",
"x-api-key": bundle.authData["api_key"],
client_id: bundle.authData["client_id"],
},
params: {
year: year,
month: month,
day: day,
page_size: "1000",
},
};
return z.request(options).then((response) => {
response.throwForStatus();
});
};
module.exports = authentication;
Do you have values in a .env file at the application root? You can also double check that console.log(process.env.API_KEY) prints a value when the test is running.
As an aside, you can use the zapier convert CLI command to copy your integration from the UI to the CLI without having to re-write it. If it works in the UI, it'll work in the CLI too.
Related
I am trying to rewrite the implementation of the Fastlane authentication in the App Store Connect Enterprise account using 2fa with Node.js . So far, I have managed to get and input a one-time password from the SMS and to get the session out of it. The relevant code is presented below:
Node.js with Typescript rewritten code
let authServiceKey = "";
try {
//sending a get request in order to get the authService (This works as intended)
const authService = await axios.get("https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com");
authServiceKey = authService.data\["authServiceKey"\];
//This request tries to sign in the and gets expected 409 error status, with some useful response data
await axios({method: 'post', url: 'https://idmsa.apple.com/appleauth/auth/signin', headers: {
// eslint-disable-next-line #typescript-eslint/naming-convention
'Content-Type': 'application/json',
// eslint-disable-next-line #typescript-eslint/naming-convention
'X-Requested-With':'XMLHttpRequest',
// eslint-disable-next-line #typescript-eslint/naming-convention
'X-Apple-Widget-Key': authServiceKey
},
data: {
accountName: process.env.APP_STORE_CONNECT_ENTERPRISE_ACCOUNT_NAME,
password: process.env.APP_STORE_CONNECT_ENTERPRISE_PASSWORD,
rememberMe: true
}
});
} catch (e:any) {
try{
const response = e["response"];
const headers = response["headers"];
const xAppleIdSessionId:string = headers["x-apple-id-session-id"];
const scnt:string = headers["scnt"];
const authService = await axios.get("https://appstoreconnect.apple.com/olympus/v1/app/config?hostname=itunesconnect.apple.com");
const authKey = authService.data["authServiceKey"];
const authenticationHeaders = {
// eslint-disable-next-line #typescript-eslint/naming-convention
'Content-Type': 'application/json',
// eslint-disable-next-line #typescript-eslint/naming-convention
'X-Apple-Id-Session-Id': xAppleIdSessionId,
// eslint-disable-next-line #typescript-eslint/naming-convention
'X-Apple-Widget-Key': authKey,
// eslint-disable-next-line #typescript-eslint/naming-convention
"Accept": "application/json",
"scnt": scnt
}
const authenticationResult = await axios({method: "get", url: "https://idmsa.apple.com/appleauth/auth", headers:authenticationHeaders });
const phoneId= authenticationResult.data["trustedPhoneNumbers"][0]["id"];
const pushMode = authenticationResult.data["trustedPhoneNumbers"][0]["pushMode"];
const body = {
phoneNumber: {
id: phoneId,
},
mode: pushMode
}
await axios({
method: 'put', url: 'https://idmsa.apple.com/appleauth/auth/verify/phone',
headers: authenticationHeaders,
data: body
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question("Input your code, received by the device ",async function (code: string) {
await axios({
method: "post", url:`https://idmsa.apple.com/appleauth/auth/verify/phone/securitycode`,
headers: authenticationHeaders, data: {
securityCode: {
code: code
},
phoneNumber:{
id: phoneId
},
mode: pushMode
}}
);
const finalRes = await axios({
method: "get",
url: "https://idmsa.apple.com/appleauth/auth/2sv/trust",
headers: authenticationHeaders
});
const sessionToCookie = finalRes["headers"]["x-apple-id-session-id"];
rl.close();
});
rl.on("close", function() {
process.exit(0);
});
}
catch (e)
{
console.error(e)
process.exit(1);
}
}
The problem occurs later since I need to use the session to create a cookie, as it is shown in the Fastlane project:
Original Ruby code:
def store_cookie(path: nil)
path ||= persistent_cookie_path
FileUtils.mkdir_p(File.expand_path("..", path))
# really important to specify the session to true
# otherwise myacinfo and more won't be stored
#cookie.save(path, :yaml, session: true)
return File.read(path)
end
def persistent_cookie_path
if ENV\["SPACESHIP_COOKIE_PATH"\]
path = File.expand_path(File.join(ENV\["SPACESHIP_COOKIE_PATH"\], "spaceship", self.user, "cookie"))
else
\[File.join(self.fastlane_user_dir, "spaceship"), "\~/.spaceship", "/var/tmp/spaceship", "#{Dir.tmpdir}/spaceship"\].each do |dir|
dir_parts = File.split(dir)
if directory_accessible?(File.expand_path(dir_parts.first))
path = File.expand_path(File.join(dir, self.user, "cookie"))
break
end
end
end
return path
end
I have no idea what to do with the received session to get the cookie based on the ruby code. Can anybody help me? And then, when I have a session, how do I access the App Store Connect
Enterprise account using cookies and avoiding the 2fa authentication based on the session?
I tried to use the tough-cookie NPM package for Node.js to build the cookie. It was not possible since I did not have options to mention YAML or session parameters.
I have 2 functions in the same google cloud functions project (myfunction1 and myfunction2.
exports.myfunction1 = async (req, res) => {
await axios({
method: 'post',
url: 'https://SERVER-PROJECT-ID.cloudfunctions.net/myfunction2',
timeout: 15000,
headers: {
'Content-Type': 'application/json',
},
data: myjson
}).then(response => {
console.log(JSON.stringify(response.data));
}).catch(err => {
console.error("catch error");
console.error(err);
})
}
It is works fine, but only if I configure invokers permission for allUsers. If I remove this permission, e receive 403 code error. Not sounds good keep this permisson activate, because the function is exposed. I tried solve with this link and this link, but, no sucess.
Edit1:
const {GoogleAuth} = require('google-auth-library');
const auth = new GoogleAuth();
const targetAudience = 'https://SERVER-PROJECT-ID.cloudfunctions.net/myfunction2'
const url = '??????????';
async function request() {
console.info('request ${url} with target audience ${targetAudience}');
const client = await auth.getIdTokenClient(targetAudience);
const res = await client.request({url});
console.info(res.data);
}
I'm trying using this code, but, who is const url?
You must perform service to service authentication. You can find a great tutorial in the Cloud Run page (ok you use Cloud Functions but the underlying infrastructure is the same and the doc is better).
You also have to be aware about the Functions identity and how to change them (or to grant the current service account the correct permission)
let audience = 'https://SERVER-PROJECT-ID.cloudfunctions.net/myfunction2';
let token_request_url = 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=' + audience;
var token_response = await axios.get(token_request_url, { headers: {'Metadata-Flavor': 'Google'} });
let token_auth = token_response.data;
axios({
method: 'post',
url: audience,
timeout: 15000,
headers: {
'Authorization': "Bearer " + token_auth
},
data: myJSON
}).catch(err => {
console.error(err);
});
I am trying to setup a restricted firebase function that can be called from another client application that runs outside GCP. So far I failed to setup the client application authentication to get passed the restricted access on the firebase funciton.
Here is what I did and tried:
I created and deployed a simple helloWorld firebase function and verified that the function could be called from the client application with the default public access.
I removed allUsers from the helloWorld permissions on GCP and verified that the function could no longer be called from the client application (I get "403 Forbidden" in the response).
I created a new service account and added it as a member of "Cloud functions invoker" in the permissions panel of helloWorld on the GCP.
I created a new private json key file for this service account.
Then I followed the documentation to setup the client application authentication (see code below).
const fetch = require('node-fetch');
const jwt = require('jsonwebtoken');
async function main(){
// get unix timestamp in seconds
const current_time = Math.floor(Date.now() / 1000)
// get the service account key file
const service_account = require('./service_account.json');
// create the jwt body
const token_body = {
"iss": service_account.client_email,
"scope": "https://www.googleapis.com/auth/cloud-platform",
"aud": "https://oauth2.googleapis.com/token",
"exp": current_time + 3600,
"iat": current_time
}
// sign the token with the private key
const signed_token = jwt.sign(
token_body, service_account.private_key, { algorithm: 'RS256' }
)
// get an access token from the authentication server
const access_token = await fetch(
'https://oauth2.googleapis.com/token',
{
method: 'POST',
body: ''
+ 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer'
+ '&'
+ 'assertion=' + signed_token,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
).then(res => res.json()).then(body => body.access_token)
// call the firebase function with the Authorization header
return fetch(
url_hello_world, { headers: { 'Authorization': 'Bearer ' + access_token } }
).then(res => res.text()).then(console.log)
}
main().catch(console.error)
Unfortunately when I run the previous code I get "401 Unauthorize" with the following header:
www-authenticate: Bearer error="invalid_token" error_description="The access token could not be verified"
After that I tried another approach with the following tutorial (see code below).
const fetch = require('node-fetch');
const util = require('util');
const exec = util.promisify(require("child_process").exec)
async function main(){
// activate a service account with a key file
await exec('gcloud auth activate-service-account --key-file=' + key_file)
// retrieve an access token for the activated service account
const {stdout, stderr} = await exec("gcloud auth print-identity-token")
// get the access token from stdout and remove the new line character at the
// end of the string
const access_token = stdout.slice(0,-1)
// call the firebase function with the Authorization header
const response = await fetch(
url_hello_world,
{ headers: { 'Authorization': 'Bearer ' + access_token } }
)
// print the response
console.log(await response.text())
}
main().catch(console.error)
When I run this code, I get the expected response "Hello World" so the previous code can call the firebase function with the service account permission.
However, the client application that I target cannot rely on the gcloud cli and I am stuck to the point where I tried to understand what does not work in the first version above and what I need to change to make it works.
As you're using a Service Account JSON file, the following code can be helpful.
package.json
{
"name": "sample-call",
"version": "0.0.1",
"dependencies": {
"googleapis": "^62.0.0",
"node-fetch": "^2.6.1"
}
}
index.js
var { google } = require('googleapis')
const fetch = require('node-fetch')
const fs = require('fs');
let privatekey = JSON.parse(fs.readFileSync('key.json'));
let url_hello_world = 'CLOUD_FUNCTION_URL';
let jwtClient = new google.auth.JWT(
privatekey.client_email,
null,
privatekey.private_key,
url_hello_world
)
async function main(){
jwtClient.authorize( async function(err, _token) {
if (err) {
console.log(err)
return err
} else {
const response = await fetch(
url_hello_world,
{
headers: { 'Authorization': 'Bearer ' + _token.id_token }
}
)
console.log(await response.text())
}
})
}
main().catch(console.error)
My goal is to consume an API which has already been deployed with nopCommerce (I do not have dev access to the server - I am just a user). There is a sample client application here, but the code is in C#. I have a webapp deployed on an Azure server using node.js. The API uses the OAuth 2.0 Authorization Code grant type.
I did a bit of Googling and it appears that client_credentials is typically used for this type of server to server flow:
How does 2-legged oauth work in OAuth 2.0?
Using OAuth for server-to-server authentication?
There is also an answer here which suggests that I can manually retrieve a token and then store it on the server. This is what I'm currently doing while testing my code.
I also found this answer which appears to be asking the same question. Like the author of that post, I am able to get a token via Postman, but my node.js code fails.
OAuth2.0 for nopCommerce
I wrote a minimal example in node.js here.
import { config } from 'dotenv';
import * as path from 'path';
import fetch from 'cross-fetch';
const ENV_FILE = path.join(__dirname, '.env');
const loadFromEnv = config({ path: ENV_FILE });
export async function getCodeUrl() {
const params = {
client_id: <CLIENT_ID>,
redirect_uri: 'http://example.com',
response_type: 'code',
};
console.log(params);
const url = new URL(`http://example.com/OAuth/Authorize`);
Object.keys(params).forEach(( key ) => url.searchParams.append(key, params[key]));
const res = await fetch(url.href, { method: 'GET' });
return res;
}
export async function getToken(code: string) {
const url = new URL(`http://example.com/api/token`);
const options = {
form: {
client_id: <CLIENT_ID>,
client_secret: <CLIENT_SECRET>,
code,
grant_type: 'authorization_code',
redirect_ui: 'http://example.com',
},
headers: { 'content-type': 'application/x-www-form-urlencoded' },
method: 'POST',
};
console.log(options);
const res = await fetch(url.href, options);
console.log('res', res);
return res;
}
const test = async () => {
const codeUrlString = (await getCodeUrl()).url;
const code = (new URL(codeUrlString).searchParams.get('code'));
if (code) {
console.log('code', code);
const tokenResponse = await getToken(code);
console.log('token res', tokenResponse);
}
};
test();
I am successfully able to retrieve the authorization code, but when I use that in a POST request to get a token, I get this error:
{ error: 'invalid_client' }
I've got an existing working test that is testing a route in an Express app (trimmed code):
const AWS = require('aws-sdk-mock');
const AWS_SDK = require('aws-sdk');
AWS.setSDKInstance(AWS_SDK);
...
before(() => {
sendEmailMock = sinon.stub().callsArgWith(1, null, 'All is well');
AWS.mock('SES', 'sendEmail', sendEmailMock);
server = rewire('../../../..');
...
describe('POST:/feedback', () => {
it('Returns 200 with a fully formed request', (done) => {
request(app)
.post('/gethelp/api/v1/feedback')
.send({
thumbsUp: 'true',
title: 'Abcdef ghi',
url: 'http://google.com',
comments: 'lkajsdj lkajsdkjf aslkjdfa asjdflasjd lkfj',
})
.expect(200, () => {
const args = sendEmailMock.args[0][0];
... etc
This is a working test. But I need to refactor it to not use the full server (because it's doing some integration stuff on startup). So I'm bringing in node-mocks-http:
const httpMocks = require('node-mocks-http');
const feedbackRouteHandler = require('./feedback');
...
before(() => {
sendEmailMock = sinon.stub().callsArgWith(1, null, 'All is well');
AWS.mock('SES', 'sendEmail', sendEmailMock);
});
...
const mockRequest = httpMocks.createRequest({
method: 'POST',
url: '/gethelp/api/v1/feedback',
body: {
thumbsUp: 'true',
title: 'Abcdef ghi',
url: 'http://google.com',
comments: 'lkajsdj lkajsdkjf aslkjdfa asjdflasjd lkfj',
},
});
const mockResponse = httpMocks.createResponse();
feedbackRouteHandler(mockRequest, mockResponse);
expect(mockResponse.statusCode).to.equal(200);
expect(sendEmailMock.args).to.exist;
The problem is that adding in node-mocks-http appears to have broken the mocking of the AWS SDK. When sendEmail is hit it's hitting the actual AWS SDK, not the mocked version. It was hitting the mocked version in the previous version of the test.
How can I use node-mocks-http with aws-sdk-mock?