Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth2 support #18

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .environment.dist
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
TEST_BASE_URL="http://..."
TEST_BASIC_AUTH_USERNAME=""
TEST_BASIC_AUTH_PASSWORD=""
BASE_URL=""
ACCESS_TOKEN=
TEST_OAUTH2_CLIENT_ID="1_6b30vz1xjv0os4go8gcssgsscokssogc0cs4cwcw0s88oswog8"
TEST_OAUTH2_CLIENT_SECRET="2yeshoapswsg0ccssoswc8wc4co4wwsosgwo4g88c4gg04wc44"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ docs
node_modules
*.log
.environment
.idea
.zapierapprc
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 3.0.0

* OAuth2 authentication support

## 2.1.7

* Minimal Node version bumped from 10 to 14.
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@ If you'd like to help with development, read the [Zapier tutorial](https://githu

There are functional tests covering the basic functionality.

1. Create `.environment` file in the root of this file and copy there content from `.environment.dist`, fill in the auth details.
2. Run `zapier test`.
1. Create new Oauth2 API credentials in your Mautic instance.
2. Create `.environment` file in the root of this file and copy there content from `.environment.dist`.
3. Fill in the `BASE_URL` (the domain where your Mautic runs), `TEST_OAUTH2_CLIENT_ID` and `TEST_OAUTH2_CLIENT_SECRET` are the credentials you've created in step 1.
4. Get access_token of Oauth2 by running `node access-token.js`
5. It will ask you to click on a URL address. When you do a browser window will open where you'll have to authorize the Oauth2 request.
6. Once you authorize it will redirect you to a NodeJs server and it will show the access token hash.
7. Fill in the `ACCESS_TOKEN` generated above to the `environment`.
8. Run `zapier test`.
80 changes: 80 additions & 0 deletions access-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const http = require('http');
const https = require('https');
const dotenv = require('dotenv');
const url = require('url');
const querystring = require('querystring');

dotenv.config({ path: '.environment'});

const serverUrl = `http://localhost:8081`;
const baseUrl = process.env.TEST_BASE_URL;
const oauthId = process.env.TEST_OAUTH2_CLIENT_ID;
const oauthSecret = process.env.TEST_OAUTH2_CLIENT_SECRET;

const requestListener = function (req, res) {
const queryObject = url.parse(req.url,true).query;
res.writeHead(200);

if (typeof queryObject.code != 'undefined') {
getAccessToken(baseUrl, oauthId, oauthSecret, serverUrl, queryObject.code, function(accessToken) {
let message = `The access token is: ${accessToken}`;
console.log(message);
res.end(message);
});
}
}

const server = http.createServer(requestListener);

server.listen(8081);

const path = `/oauth/v2/authorize?client_id=${oauthId}&grant_type=authorization_code&redirect_uri=${querystring.escape(serverUrl)}&response_type=code`;

console.log(`Click on this URL to authenticate and get your access token: ${baseUrl+path}`);


function getAccessToken(baseUrl, clientId, clientSecret, reditectUrl, code, resolve) {

const data = querystring.stringify({
client_id: clientId,
client_secret: clientSecret,
grant_type: 'authorization_code',
redirect_uri: reditectUrl,
code: code,
});

const host = baseUrl.replace(/(^\w+:|^)\/\//, '');

const req = https.request({
host: host,
port: 443,
path: '/oauth/v2/token',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
'Content-Length': Buffer.byteLength(data)
},
rejectUnauthorized: false,
requestCert: true,
agent: false
}, res => {
const body = []

res.on('data', chunk => {
body.push(chunk);
})

res.on('end', () => {
const resString = Buffer.concat(body).toString()
resolve(JSON.parse(resString).access_token)
})
})

req.on('error', error => {
console.error(error)
resolve('error happened:' + error);
})

req.write(data)
req.end()
}
118 changes: 100 additions & 18 deletions authentication.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,112 @@
const test = (z, bundle) => {
// Normally you want to make a request to an endpoint that is either specifically designed to test auth, or one that
// every user will have access to, such as an account or profile endpoint like /me.
// In this example, we'll hit httpbin, which validates the Authorization Header against the arguments passed in the URL path
const getAccessToken = (z, bundle) => {
const promise = z.request(bundle.inputData.baseUrl+'/oauth/v2/token', {
method: 'POST',
body: {
// extra data pulled from the users query string
accountDomain: bundle.cleanedRequest.querystring.accountDomain,
code: bundle.inputData.code,
client_id: bundle.inputData.clientId,
client_secret: bundle.inputData.clientSecret,
redirect_uri: bundle.inputData.redirect_uri,
grant_type: 'authorization_code'
},
headers: {
'content-type': 'application/x-www-form-urlencoded'
}
});

// Needs to return `access_token` and refresh_token
return promise.then(response => {
if (response.status !== 200) {
throw new Error('Unable to fetch access token: ' + response.content);
}

const result = JSON.parse(response.content);
return {
access_token: result.access_token,
refresh_token: result.refresh_token
};
});
};

const refreshAccessToken = (z, bundle) => {
const promise = z.request(bundle.authData.baseUrl+'/oauth/v2/token', {
method: 'POST',
body: {
refresh_token: bundle.authData.refresh_token,
client_id: bundle.authData.clientId,
client_secret: bundle.authData.clientSecret,
grant_type: 'refresh_token'
},
headers: {
'content-type': 'application/x-www-form-urlencoded'
}
});

// Needs to return `access_token` and refresh_token
return promise.then(response => {
if (response.status !== 200) {
throw new Error('Unable to fetch access token: ' + response.content);
}

const result = JSON.parse(response.content);
return {
access_token: result.access_token,
refresh_token: result.refresh_token
};
});
};


const testAuth = (z , bundle ) => {
// Normally you want to make a request to an endpoint that is either specifically designed to test auth, or one that every user will have access to, such as an account or profile endpoint like /me.
const promise = z.request({
method: 'GET',
url: bundle.authData.baseUrl+'/api/contacts/list/fields',
});

// This method can return any truthy value to indicate the credentials are valid.
// Raise an error to show
return z.request({
url: bundle.authData.baseUrl+'/api/contacts?limit=1&minimal=1',
}).then((response) => {
return promise.then(response => {

if (typeof response.json === 'undefined') {
throw new Error('The URL you provided ('+bundle.authData.baseUrl+') is not the base URL of a Mautic instance');
}
if (typeof response.json === 'undefined') {
throw new Error('The URL you provided ('+bundle.inputData.baseUrl+') is not the base URL of a Mautic instance');
}

return response;
});
return response;
});
};


module.exports = {
type: 'basic',
type: 'oauth2',
fields: [
{key: 'clientId', type: 'string', required: true, helpText: 'Your Client ID (Public Key) is available in Mautic > Settings > API Credentials > OAuth 2'},
{key: 'clientSecret', type: 'string', required: true, helpText: 'Your Client Secret (Secret Key) is available in Mautic > Settings > API Credentials > OAuth 2'},
{key: 'baseUrl', type: 'string', required: true, helpText: 'The root URL of your Mautic installation starting with https://. E.g. https://my.mautic.net.'}
],
connectionLabel: '{{bundle.authData.username}}',

// The test method allows Zapier to verify that the credentials a user provides are valid. We'll execute this
// method whenver a user connects their account for the first time.
test: test
oauth2Config: {
// Step 1 of the OAuth flow; specify where to send the user to authenticate with Mautic API.
// Zapier generates the state and redirect_uri, you are responsible for providing the rest.
authorizeUrl: {
url: '{{bundle.inputData.baseUrl}}/oauth/v2/authorize',
params: {
client_id: '{{bundle.inputData.clientId}}',
state: '{{bundle.inputData.state}}',
redirect_uri: '{{bundle.inputData.redirect_uri}}',
response_type: 'code'
}
},
// Step 2 of the OAuth flow; Exchange a code for an access token.
// This could also use the request shorthand.
getAccessToken: getAccessToken,
// The access token expires after a pre-defined amount of time, these method refresh it
refreshAccessToken: refreshAccessToken,
// Zapier to automatically invoke `refreshAccessToken` on a 401 response
autoRefresh: true
},
// The test method allows Zapier to verify that the access token is valid
test: testAuth,
// assuming "baseUrl of Mautic"
connectionLabel: '{{baseUrl}}',
};
4 changes: 2 additions & 2 deletions entities/contact.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Contact = function(z, bundle) {
// Fill in the core fields we want to provide
coreFields.forEach((field) => {
var type = typeof dirtyContact[field.key];
if (type !== 'undefined' && (type === 'string' || type === 'number')) {
if (dirtyContact[field.key] == null || (type !== 'undefined' && (type === 'string' || type === 'number'))) {
contact[field.key] = dirtyContact[field.key];
}
});
Expand All @@ -45,7 +45,7 @@ Contact = function(z, bundle) {
}
}
}

// Flatten the owner info
if (dirtyContact.owner && typeof dirtyContact.owner === 'object' && dirtyContact.owner.id) {
contact.ownedBy = dirtyContact.owner.id;
Expand Down
6 changes: 5 additions & 1 deletion middlewares/beforeRequest.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
const sanitizeUrl = (request, z) => {
const sanitizeUrl = (request, z, bundle) => {

// remove double slashes if the user submitted the Mautic URL with trailing slash
if (request.url) {
request.url = request.url.replace('//api', '/api');
}

if (bundle.authData && bundle.authData.access_token) {
request.headers.Authorization = `Bearer ${bundle.authData.access_token}`;
}

return request;
};

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading