From 25885cbbc99bfd867a67906b76b37d0243c32e6e Mon Sep 17 00:00:00 2001
From: Richard Simpson <richardsimpson@outlook.com>
Date: Sat, 4 Apr 2020 12:50:37 -0500
Subject: [PATCH] feat: add generic auth

Adds the ability to authenticate against any normal Vault endpoint by added the uthPayload input.

When an unrecognized method is provided, the action will attempt to hit 1/auth/<method>/login with the provided uthPayload and parse out the token in the response
---
 action.yml                           |   3 +
 dist/index.js                        | 671 ++++++++++++++-------------
 package.json                         |   2 +-
 action.js => src/action.js           |  51 +-
 action.test.js => src/action.test.js |   0
 src/auth.js                          |  59 +++
 index.js => src/index.js             |   0
 7 files changed, 408 insertions(+), 378 deletions(-)
 rename action.js => src/action.js (79%)
 rename action.test.js => src/action.test.js (100%)
 create mode 100644 src/auth.js
 rename index.js => src/index.js (100%)

diff --git a/action.yml b/action.yml
index 9c1d7a9..bbb7281 100644
--- a/action.yml
+++ b/action.yml
@@ -33,6 +33,9 @@ inputs:
   githubToken:
     description: 'The Github Token to be used to authenticate with Vault'
     required: false
+  authPayload:
+    description: 'The JSON payload to be sent to Vault when using a custom authentication method.'
+    required: false
   extraHeaders:
     description: 'A string of newline separated extra headers to include on every request.'
     required: false
diff --git a/dist/index.js b/dist/index.js
index a658c33..bb3cd8c 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -34,7 +34,7 @@ module.exports =
 /******/ 	// the startup function
 /******/ 	function startup() {
 /******/ 		// Load entry module and return exports
-/******/ 		return __webpack_require__(104);
+/******/ 		return __webpack_require__(676);
 /******/ 	};
 /******/ 	// initialize runtime
 /******/ 	runtime(__webpack_require__);
@@ -805,22 +805,6 @@ class Response extends Readable {
 module.exports = Response;
 
 
-/***/ }),
-
-/***/ 104:
-/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) {
-
-const core = __webpack_require__(470);
-const { exportSecrets } = __webpack_require__(751);
-
-(async () => {
-    try {
-        await core.group('Get Vault Secrets', exportSecrets);
-    } catch (error) {
-        core.setFailed(error.message);
-    }
-})();
-
 /***/ }),
 
 /***/ 110:
@@ -1568,6 +1552,72 @@ module.exports.iterator = (emitter, event, options) => {
 };
 
 
+/***/ }),
+
+/***/ 151:
+/***/ (function(module, __unusedexports, __webpack_require__) {
+
+const core = __webpack_require__(470);
+
+/***
+ * Authentication with Vault and retrieve a vault token
+ * @param {string} method
+ * @param {import('got')} client
+ */
+async function retrieveToken(method, client) {
+    switch (method) {
+        case 'approle': {
+            const vaultRoleId = core.getInput('roleId', { required: true });
+            const vaultSecretId = core.getInput('secretId', { required: true });
+            return await getClientToken(client, method, { role_id: vaultRoleId, secret_id: vaultSecretId });
+        }
+        case 'github': {
+            const githubToken = core.getInput('githubToken', { required: true });
+            return await getClientToken(client, method, { token: githubToken });
+        }
+        default: {
+            if (!method || method === 'token') {
+                return core.getInput('token', { required: true });
+            } else {
+                /** @type {string} */
+                const payload = core.getInput('authPayload', { required: true });
+                if (!payload) {
+                    throw Error('When using a custom authentication method, you must provide the payload');
+                }
+                return await getClientToken(client, method, JSON.parse(payload.trim()));
+            }
+        }
+    }
+}
+
+/***
+ * Authentication with Vault and retrieve a vault token
+ * @param {import('got')} client
+ * @param {string} method
+ * @param {any} payload
+ */
+async function getClientToken(client, method, payload) {
+    /** @type {any} */
+    var options = {
+        json: payload,
+        responseType: 'json'
+    };
+
+    core.debug(`Retrieving Vault Token from v1/auth/${method}/login endpoint`);
+    const response = await client.post(`v1/auth/${method}/login`, options);
+    if (response && response.body && response.body.auth && response.body.auth.client_token) {
+        core.debug('✔ Vault Token successfully retrieved');
+        return response.body.auth.client_token;
+    } else {
+        throw Error(`Unable to retrieve token from ${method}'s login endpoint.`);
+    }
+}
+
+module.exports = {
+    retrieveToken
+}
+
+
 /***/ }),
 
 /***/ 154:
@@ -4647,6 +4697,22 @@ module.exports = require("util");
 
 /***/ }),
 
+/***/ 676:
+/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) {
+
+const core = __webpack_require__(470);
+const { exportSecrets } = __webpack_require__(928);
+
+(async () => {
+    try {
+        await core.group('Get Vault Secrets', exportSecrets);
+    } catch (error) {
+        core.setFailed(error.message);
+    }
+})();
+
+/***/ }),
+
 /***/ 678:
 /***/ (function(__unusedmodule, exports, __webpack_require__) {
 
@@ -4872,316 +4938,6 @@ module.exports.DuplexWrapper = DuplexWrapper;
 
 module.exports = require("fs");
 
-/***/ }),
-
-/***/ 751:
-/***/ (function(module, __unusedexports, __webpack_require__) {
-
-// @ts-check
-
-// @ts-ignore
-const core = __webpack_require__(470);
-// @ts-ignore
-const command = __webpack_require__(431);
-const got = __webpack_require__(77);
-
-const AUTH_METHODS = ['approle', 'token', 'github'];
-const VALID_KV_VERSION = [-1, 1, 2];
-
-async function exportSecrets() {
-    const vaultUrl = core.getInput('url', { required: true });
-    const vaultNamespace = core.getInput('namespace', { required: false });
-    const extraHeaders = parseHeadersInput('extraHeaders', { required: false });
-    const exportEnv = core.getInput('exportEnv', { required: false }) != 'false';
-
-    let enginePath = core.getInput('path', { required: false });
-    let kvVersion = core.getInput('kv-version', { required: false });
-
-    const secretsInput = core.getInput('secrets', { required: true });
-    const secretRequests = parseSecretsInput(secretsInput);
-
-    const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
-    if (!AUTH_METHODS.includes(vaultMethod)) {
-        throw Error(`Sorry, the authentication method ${vaultMethod} is not currently supported.`);
-    }
-
-    const defaultOptions = {
-        prefixUrl: vaultUrl,
-        headers: {}
-    }
-
-    for (const [headerName, headerValue] of extraHeaders) {
-        defaultOptions.headers[headerName] = headerValue;
-    }
-
-    if (vaultNamespace != null) {
-        defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace;
-    }
-
-    const client = got.extend(defaultOptions);
-    const vaultToken = await retrieveToken(vaultMethod, /** @type {any} */ (client));
-
-    if (!enginePath) {
-        enginePath = 'secret';
-    }
-
-    if (!kvVersion) {
-        kvVersion = 2;
-    }
-    kvVersion = +kvVersion;
-
-    if (Number.isNaN(kvVersion) || !VALID_KV_VERSION.includes(kvVersion)) {
-        throw Error(`You must provide a valid K/V version (${VALID_KV_VERSION.slice(1).join(', ')}). Input: "${kvVersion}"`);
-    }
-
-    const responseCache = new Map();
-    for (const secretRequest of secretRequests) {
-        const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest;
-        const requestOptions = {
-            headers: {
-                'X-Vault-Token': vaultToken
-            },
-        };
-
-        for (const [headerName, headerValue] of extraHeaders) {
-            requestOptions.headers[headerName] = headerValue;
-        }
-
-        if (vaultNamespace != null) {
-            requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
-        }
-
-        let requestPath = `v1`;
-        const kvRequest = !secretPath.startsWith('/')
-        if (!kvRequest) {
-            requestPath += secretPath;
-        } else {
-           requestPath += (kvVersion === 2)
-                    ? `/${enginePath}/data/${secretPath}`
-                    : `/${enginePath}/${secretPath}`;
-        }
-
-        let body;
-        if (responseCache.has(requestPath)) {
-            body = responseCache.get(requestPath);
-            core.debug('ℹ using cached response');
-        } else {
-            const result = await client.get(requestPath, requestOptions);
-            body = result.body;
-            responseCache.set(requestPath, body);
-        }
-
-        let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion;
-
-        const secretData = getResponseData(body, dataDepth);
-        const value = selectData(secretData, secretSelector, isJSONPath);
-        command.issue('add-mask', value);
-        if (exportEnv) {
-            core.exportVariable(envVarName, `${value}`);
-        }
-        core.setOutput(outputVarName, `${value}`);
-        core.debug(`✔ ${secretPath} => outputs.${outputVarName}${exportEnv ? ` | env.${envVarName}` : ''}`);
-    }
-};
-
-/** @typedef {Object} SecretRequest 
- * @property {string} secretPath
- * @property {string} envVarName
- * @property {string} outputVarName
- * @property {string} secretSelector
- * @property {boolean} isJSONPath
-*/
-
-/**
- * Parses a secrets input string into key paths and their resulting environment variable name.
- * @param {string} secretsInput
- */
-function parseSecretsInput(secretsInput) {
-    const secrets = secretsInput
-        .split(';')
-        .filter(key => !!key)
-        .map(key => key.trim())
-        .filter(key => key.length !== 0);
-
-    /** @type {SecretRequest[]} */
-    const output = [];
-    for (const secret of secrets) {
-        let path = secret;
-        let outputVarName = null;
-
-        const renameSigilIndex = secret.lastIndexOf('|');
-        if (renameSigilIndex > -1) {
-            path = secret.substring(0, renameSigilIndex).trim();
-            outputVarName = secret.substring(renameSigilIndex + 1).trim();
-
-            if (outputVarName.length < 1) {
-                throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`);
-            }
-        }
-
-        const pathParts = path
-            .split(/\s+/)
-            .map(part => part.trim())
-            .filter(part => part.length !== 0);
-
-        if (pathParts.length !== 2) {
-            throw Error(`You must provide a valid path and key. Input: "${secret}"`);
-        }
-
-        const [secretPath, secretSelector] = pathParts;
-
-        const isJSONPath = secretSelector.includes('.');
-
-        if (isJSONPath && !outputVarName) {
-            throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`);
-        }
-
-        let envVarName = outputVarName;
-        if (!outputVarName) {
-            outputVarName = secretSelector;
-            envVarName = normalizeOutputKey(outputVarName);
-        }
-
-        output.push({
-            secretPath,
-            envVarName,
-            outputVarName,
-            secretSelector,
-            isJSONPath
-        });
-    }
-    return output;
-}
-
-/***
- * Authentication with Vault and retrieve a vault token
- * @param {string} method
- * @param {import('got')} client
- */
-async function retrieveToken(method, client) {
-    switch (method) {
-        case 'approle': {
-            const vaultRoleId = core.getInput('roleId', { required: true });
-            const vaultSecretId = core.getInput('secretId', { required: true });
-            core.debug('Try to retrieve Vault Token from approle');
-
-            /** @type {any} */
-            var options = {
-                json: { role_id: vaultRoleId, secret_id: vaultSecretId },
-                responseType: 'json'
-            };
-
-            const result = await client.post(`v1/auth/approle/login`, options);
-            if (result && result.body && result.body.auth && result.body.auth.client_token) {
-                core.debug('✔ Vault Token has retrieved from approle');
-                return result.body.auth.client_token;
-            } else {
-                throw Error(`No token was retrieved with the role_id and secret_id provided.`);
-            }
-        }
-        case 'github': {
-            const githubToken = core.getInput('githubToken', { required: true });
-            core.debug('Try to retrieve Vault Token from approle');
-
-            /** @type {any} */
-            var options = {
-                json: { token: githubToken },
-                responseType: 'json'
-            };
-
-            const result = await client.post(`v1/auth/github/login`, options);
-            if (result && result.body && result.body.auth && result.body.auth.client_token) {
-                core.debug('✔ Vault Token has retrieved from approle');
-                return result.body.auth.client_token;
-            } else {
-                throw Error(`No token was retrieved with the role_id and secret_id provided.`);
-            }
-        }
-        default:
-            return core.getInput('token', { required: true });
-    }
-}
-
-/**
- * Parses a JSON response and returns the secret data
- * @param {string} responseBody
- * @param {number} dataLevel
- */
-function getResponseData(responseBody, dataLevel) {
-    let secretData = JSON.parse(responseBody);
-
-    for (let i = 0; i < dataLevel; i++) {
-        secretData = secretData['data'];
-    }
-    return secretData;
-}
-
-/**
- * Parses a JSON response and returns the secret data
- * @param {Object} data
- * @param {string} selector
- */
-function selectData(data, selector, isJSONPath) {
-    if (!isJSONPath) {
-        return data[selector];
-    }
-
-    // TODO: JSONPath
-}
-
-/**
- * Replaces any forward-slash characters to
- * @param {string} dataKey
- */
-function normalizeOutputKey(dataKey) {
-    return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase();
-}
-
-// @ts-ignore
-/**
- * @param {string} input
- */
-function parseBoolInput(input) {
-    if (input === null || input === undefined || input.trim() === '') {
-        return null;
-    }
-    return Boolean(input);
-}
-
-/**
- * @param {string} inputKey
- * @param {any} inputOptions
- */
-function parseHeadersInput(inputKey, inputOptions) {
-    /** @type {string}*/
-    const rawHeadersString = core.getInput(inputKey, inputOptions) || '';
-    const headerStrings = rawHeadersString
-        .split('\n')
-        .map(line => line.trim())
-        .filter(line => line !== '');
-    return headerStrings
-        .reduce((map, line) => {
-            const seperator = line.indexOf(':');
-            const key = line.substring(0, seperator).trim().toLowerCase();
-            const value = line.substring(seperator + 1).trim();
-            if (map.has(key)) {
-                map.set(key, [map.get(key), value].join(', '));
-            } else {
-                map.set(key, value);
-            }
-            return map;
-        }, new Map());
-}
-
-module.exports = {
-    exportSecrets,
-    parseSecretsInput,
-    parseResponse: getResponseData,
-    normalizeOutputKey,
-    parseHeadersInput
-};
-
-
 /***/ }),
 
 /***/ 753:
@@ -5949,6 +5705,267 @@ exports.proxyEvents = (proxy, emitter) => {
 
 module.exports = require("dns");
 
+/***/ }),
+
+/***/ 928:
+/***/ (function(module, __unusedexports, __webpack_require__) {
+
+// @ts-check
+// @ts-ignore
+const core = __webpack_require__(470);
+// @ts-ignore
+const command = __webpack_require__(431);
+const got = __webpack_require__(77);
+const { retrieveToken } = __webpack_require__(151);
+
+const AUTH_METHODS = ['approle', 'token', 'github'];
+const VALID_KV_VERSION = [-1, 1, 2];
+
+async function exportSecrets() {
+    const vaultUrl = core.getInput('url', { required: true });
+    const vaultNamespace = core.getInput('namespace', { required: false });
+    const extraHeaders = parseHeadersInput('extraHeaders', { required: false });
+    const exportEnv = core.getInput('exportEnv', { required: false }) != 'false';
+
+    let enginePath = core.getInput('path', { required: false });
+    let kvVersion = core.getInput('kv-version', { required: false });
+
+    const secretsInput = core.getInput('secrets', { required: true });
+    const secretRequests = parseSecretsInput(secretsInput);
+
+    const vaultMethod = (core.getInput('method', { required: false }) || 'token').toLowerCase();
+    if (!AUTH_METHODS.includes(vaultMethod)) {
+        throw Error(`Sorry, the authentication method ${vaultMethod} is not currently supported.`);
+    }
+
+    const defaultOptions = {
+        prefixUrl: vaultUrl,
+        headers: {}
+    }
+
+    for (const [headerName, headerValue] of extraHeaders) {
+        defaultOptions.headers[headerName] = headerValue;
+    }
+
+    if (vaultNamespace != null) {
+        defaultOptions.headers["X-Vault-Namespace"] = vaultNamespace;
+    }
+
+    const client = got.extend(defaultOptions);
+    const vaultToken = await retrieveToken(vaultMethod, /** @type {any} */ (client));
+
+    if (!enginePath) {
+        enginePath = 'secret';
+    }
+
+    if (!kvVersion) {
+        kvVersion = 2;
+    }
+    kvVersion = +kvVersion;
+
+    if (Number.isNaN(kvVersion) || !VALID_KV_VERSION.includes(kvVersion)) {
+        throw Error(`You must provide a valid K/V version (${VALID_KV_VERSION.slice(1).join(', ')}). Input: "${kvVersion}"`);
+    }
+
+    const responseCache = new Map();
+    for (const secretRequest of secretRequests) {
+        const { secretPath, outputVarName, envVarName, secretSelector, isJSONPath } = secretRequest;
+        const requestOptions = {
+            headers: {
+                'X-Vault-Token': vaultToken
+            },
+        };
+
+        for (const [headerName, headerValue] of extraHeaders) {
+            requestOptions.headers[headerName] = headerValue;
+        }
+
+        if (vaultNamespace != null) {
+            requestOptions.headers["X-Vault-Namespace"] = vaultNamespace;
+        }
+
+        let requestPath = `v1`;
+        const kvRequest = !secretPath.startsWith('/')
+        if (!kvRequest) {
+            requestPath += secretPath;
+        } else {
+           requestPath += (kvVersion === 2)
+                    ? `/${enginePath}/data/${secretPath}`
+                    : `/${enginePath}/${secretPath}`;
+        }
+
+        let body;
+        if (responseCache.has(requestPath)) {
+            body = responseCache.get(requestPath);
+            core.debug('ℹ using cached response');
+        } else {
+            const result = await client.get(requestPath, requestOptions);
+            body = result.body;
+            responseCache.set(requestPath, body);
+        }
+
+        let dataDepth = isJSONPath === true ? 0 : kvRequest === false ? 1 : kvVersion;
+
+        const secretData = getResponseData(body, dataDepth);
+        const value = selectData(secretData, secretSelector, isJSONPath);
+        command.issue('add-mask', value);
+        if (exportEnv) {
+            core.exportVariable(envVarName, `${value}`);
+        }
+        core.setOutput(outputVarName, `${value}`);
+        core.debug(`✔ ${secretPath} => outputs.${outputVarName}${exportEnv ? ` | env.${envVarName}` : ''}`);
+    }
+};
+
+/** @typedef {Object} SecretRequest 
+ * @property {string} secretPath
+ * @property {string} envVarName
+ * @property {string} outputVarName
+ * @property {string} secretSelector
+ * @property {boolean} isJSONPath
+*/
+
+/**
+ * Parses a secrets input string into key paths and their resulting environment variable name.
+ * @param {string} secretsInput
+ */
+function parseSecretsInput(secretsInput) {
+    const secrets = secretsInput
+        .split(';')
+        .filter(key => !!key)
+        .map(key => key.trim())
+        .filter(key => key.length !== 0);
+
+    /** @type {SecretRequest[]} */
+    const output = [];
+    for (const secret of secrets) {
+        let path = secret;
+        let outputVarName = null;
+
+        const renameSigilIndex = secret.lastIndexOf('|');
+        if (renameSigilIndex > -1) {
+            path = secret.substring(0, renameSigilIndex).trim();
+            outputVarName = secret.substring(renameSigilIndex + 1).trim();
+
+            if (outputVarName.length < 1) {
+                throw Error(`You must provide a value when mapping a secret to a name. Input: "${secret}"`);
+            }
+        }
+
+        const pathParts = path
+            .split(/\s+/)
+            .map(part => part.trim())
+            .filter(part => part.length !== 0);
+
+        if (pathParts.length !== 2) {
+            throw Error(`You must provide a valid path and key. Input: "${secret}"`);
+        }
+
+        const [secretPath, secretSelector] = pathParts;
+
+        const isJSONPath = secretSelector.includes('.');
+
+        if (isJSONPath && !outputVarName) {
+            throw Error(`You must provide a name for the output key when using json selectors. Input: "${secret}"`);
+        }
+
+        let envVarName = outputVarName;
+        if (!outputVarName) {
+            outputVarName = secretSelector;
+            envVarName = normalizeOutputKey(outputVarName);
+        }
+
+        output.push({
+            secretPath,
+            envVarName,
+            outputVarName,
+            secretSelector,
+            isJSONPath
+        });
+    }
+    return output;
+}
+
+/**
+ * Parses a JSON response and returns the secret data
+ * @param {string} responseBody
+ * @param {number} dataLevel
+ */
+function getResponseData(responseBody, dataLevel) {
+    let secretData = JSON.parse(responseBody);
+
+    for (let i = 0; i < dataLevel; i++) {
+        secretData = secretData['data'];
+    }
+    return secretData;
+}
+
+/**
+ * Parses a JSON response and returns the secret data
+ * @param {Object} data
+ * @param {string} selector
+ */
+function selectData(data, selector, isJSONPath) {
+    if (!isJSONPath) {
+        return data[selector];
+    }
+
+    // TODO: JSONPath
+}
+
+/**
+ * Replaces any forward-slash characters to
+ * @param {string} dataKey
+ */
+function normalizeOutputKey(dataKey) {
+    return dataKey.replace('/', '__').replace(/[^\w-]/, '').toUpperCase();
+}
+
+// @ts-ignore
+/**
+ * @param {string} input
+ */
+function parseBoolInput(input) {
+    if (input === null || input === undefined || input.trim() === '') {
+        return null;
+    }
+    return Boolean(input);
+}
+
+/**
+ * @param {string} inputKey
+ * @param {any} inputOptions
+ */
+function parseHeadersInput(inputKey, inputOptions) {
+    /** @type {string}*/
+    const rawHeadersString = core.getInput(inputKey, inputOptions) || '';
+    const headerStrings = rawHeadersString
+        .split('\n')
+        .map(line => line.trim())
+        .filter(line => line !== '');
+    return headerStrings
+        .reduce((map, line) => {
+            const seperator = line.indexOf(':');
+            const key = line.substring(0, seperator).trim().toLowerCase();
+            const value = line.substring(seperator + 1).trim();
+            if (map.has(key)) {
+                map.set(key, [map.get(key), value].join(', '));
+            } else {
+                map.set(key, value);
+            }
+            return map;
+        }, new Map());
+}
+
+module.exports = {
+    exportSecrets,
+    parseSecretsInput,
+    parseResponse: getResponseData,
+    normalizeOutputKey,
+    parseHeadersInput
+};
+
+
 /***/ }),
 
 /***/ 946:
diff --git a/package.json b/package.json
index 7c12b6f..4a575bd 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
     "description": "A Github Action that allows you to consume vault secrets as secure environment variables.",
     "main": "dist/index.js",
     "scripts": {
-        "build": "ncc build index.js -o dist",
+        "build": "ncc build src/index.js -o dist",
         "test": "jest",
         "test:integration:basic": "jest -c integrationTests/basic/jest.config.js",
         "test:integration:enterprise": "jest -c integrationTests/enterprise/jest.config.js",
diff --git a/action.js b/src/action.js
similarity index 79%
rename from action.js
rename to src/action.js
index 555c1e8..e25ef22 100644
--- a/action.js
+++ b/src/action.js
@@ -1,10 +1,10 @@
 // @ts-check
-
 // @ts-ignore
 const core = require('@actions/core');
 // @ts-ignore
 const command = require('@actions/core/lib/command');
 const got = require('got');
+const { retrieveToken } = require('./auth');
 
 const AUTH_METHODS = ['approle', 'token', 'github'];
 const VALID_KV_VERSION = [-1, 1, 2];
@@ -174,55 +174,6 @@ function parseSecretsInput(secretsInput) {
     return output;
 }
 
-/***
- * Authentication with Vault and retrieve a vault token
- * @param {string} method
- * @param {import('got')} client
- */
-async function retrieveToken(method, client) {
-    switch (method) {
-        case 'approle': {
-            const vaultRoleId = core.getInput('roleId', { required: true });
-            const vaultSecretId = core.getInput('secretId', { required: true });
-            core.debug('Try to retrieve Vault Token from approle');
-
-            /** @type {any} */
-            var options = {
-                json: { role_id: vaultRoleId, secret_id: vaultSecretId },
-                responseType: 'json'
-            };
-
-            const result = await client.post(`v1/auth/approle/login`, options);
-            if (result && result.body && result.body.auth && result.body.auth.client_token) {
-                core.debug('✔ Vault Token has retrieved from approle');
-                return result.body.auth.client_token;
-            } else {
-                throw Error(`No token was retrieved with the role_id and secret_id provided.`);
-            }
-        }
-        case 'github': {
-            const githubToken = core.getInput('githubToken', { required: true });
-            core.debug('Try to retrieve Vault Token from approle');
-
-            /** @type {any} */
-            var options = {
-                json: { token: githubToken },
-                responseType: 'json'
-            };
-
-            const result = await client.post(`v1/auth/github/login`, options);
-            if (result && result.body && result.body.auth && result.body.auth.client_token) {
-                core.debug('✔ Vault Token has retrieved from approle');
-                return result.body.auth.client_token;
-            } else {
-                throw Error(`No token was retrieved with the role_id and secret_id provided.`);
-            }
-        }
-        default:
-            return core.getInput('token', { required: true });
-    }
-}
-
 /**
  * Parses a JSON response and returns the secret data
  * @param {string} responseBody
diff --git a/action.test.js b/src/action.test.js
similarity index 100%
rename from action.test.js
rename to src/action.test.js
diff --git a/src/auth.js b/src/auth.js
new file mode 100644
index 0000000..c6dabe4
--- /dev/null
+++ b/src/auth.js
@@ -0,0 +1,59 @@
+const core = require('@actions/core');
+
+/***
+ * Authentication with Vault and retrieve a vault token
+ * @param {string} method
+ * @param {import('got')} client
+ */
+async function retrieveToken(method, client) {
+    switch (method) {
+        case 'approle': {
+            const vaultRoleId = core.getInput('roleId', { required: true });
+            const vaultSecretId = core.getInput('secretId', { required: true });
+            return await getClientToken(client, method, { role_id: vaultRoleId, secret_id: vaultSecretId });
+        }
+        case 'github': {
+            const githubToken = core.getInput('githubToken', { required: true });
+            return await getClientToken(client, method, { token: githubToken });
+        }
+        default: {
+            if (!method || method === 'token') {
+                return core.getInput('token', { required: true });
+            } else {
+                /** @type {string} */
+                const payload = core.getInput('authPayload', { required: true });
+                if (!payload) {
+                    throw Error('When using a custom authentication method, you must provide the payload');
+                }
+                return await getClientToken(client, method, JSON.parse(payload.trim()));
+            }
+        }
+    }
+}
+
+/***
+ * Authentication with Vault and retrieve a vault token
+ * @param {import('got')} client
+ * @param {string} method
+ * @param {any} payload
+ */
+async function getClientToken(client, method, payload) {
+    /** @type {any} */
+    var options = {
+        json: payload,
+        responseType: 'json'
+    };
+
+    core.debug(`Retrieving Vault Token from v1/auth/${method}/login endpoint`);
+    const response = await client.post(`v1/auth/${method}/login`, options);
+    if (response && response.body && response.body.auth && response.body.auth.client_token) {
+        core.debug('✔ Vault Token successfully retrieved');
+        return response.body.auth.client_token;
+    } else {
+        throw Error(`Unable to retrieve token from ${method}'s login endpoint.`);
+    }
+}
+
+module.exports = {
+    retrieveToken
+}
diff --git a/index.js b/src/index.js
similarity index 100%
rename from index.js
rename to src/index.js
