From 4e3b0f820c3ff99aad6cccd6d3874aca95d25a50 Mon Sep 17 00:00:00 2001 From: strausr Date: Thu, 3 Jun 2021 12:31:42 +0300 Subject: [PATCH] Add support for oauth authorization (#489) --- lib-es5/api_client/call_api.js | 16 ++- lib-es5/api_client/execute_request.js | 11 +- lib-es5/uploader.js | 7 +- lib-es5/utils/index.js | 3 + lib/api_client/call_api.js | 16 ++- lib/api_client/execute_request.js | 11 +- lib/uploader.js | 7 +- lib/utils/index.js | 3 + .../authorization/oAuth_authorization_spec.js | 128 ++++++++++++++++++ types/index.d.ts | 3 + 10 files changed, 189 insertions(+), 16 deletions(-) create mode 100644 test/integration/api/authorization/oAuth_authorization_spec.js diff --git a/lib-es5/api_client/call_api.js b/lib-es5/api_client/call_api.js index 01c0ba12..844199d0 100644 --- a/lib-es5/api_client/call_api.js +++ b/lib-es5/api_client/call_api.js @@ -12,11 +12,17 @@ var ensurePresenceOf = utils.ensurePresenceOf; function call_api(method, uri, params, callback, options) { ensurePresenceOf({ method, uri }); var api_url = utils.base_api_url(uri, options); - var auth = { - key: ensureOption(options, "api_key"), - secret: ensureOption(options, "api_secret") - }; - + var auth = {}; + if (options.oauth_token || config().oauth_token) { + auth = { + oauth_token: ensureOption(options, "oauth_token") + }; + } else { + auth = { + key: ensureOption(options, "api_key"), + secret: ensureOption(options, "api_secret") + }; + } return execute_request(method, params, auth, api_url, callback, options); } diff --git a/lib-es5/api_client/execute_request.js b/lib-es5/api_client/execute_request.js index d6e1abf7..a385fbd9 100644 --- a/lib-es5/api_client/execute_request.js +++ b/lib-es5/api_client/execute_request.js @@ -23,6 +23,7 @@ function execute_request(method, params, auth, api_url, callback) { handle_response = void 0; // declare to user later var key = auth.key; var secret = auth.secret; + var oauth_token = auth.oauth_token; var content_type = 'application/x-www-form-urlencoded'; if (options.content_type === 'json') { @@ -43,9 +44,15 @@ function execute_request(method, params, auth, api_url, callback) { headers: { 'Content-Type': content_type, 'User-Agent': utils.getUserAgent() - }, - auth: key + ":" + secret + } }); + + if (oauth_token) { + request_options.headers.Authorization = `Bearer ${oauth_token}`; + } else { + request_options.auth = key + ":" + secret; + } + if (options.agent != null) { request_options.agent = options.agent; } diff --git a/lib-es5/uploader.js b/lib-es5/uploader.js index f67cab4f..f45979a4 100644 --- a/lib-es5/uploader.js +++ b/lib-es5/uploader.js @@ -33,6 +33,7 @@ var https = isSecure ? require('https') : require('http'); var Cache = require('./cache'); var utils = require("./utils"); var UploadStream = require('./upload_stream'); +var config = require("./config"); var build_upload_params = utils.build_upload_params, extend = utils.extend, @@ -558,7 +559,6 @@ function call_api(action, callback, options, get_params) { return Buffer.from(encodeFieldPart(boundary, key, value), 'utf8'); }); - var result = post(api_url, post_data, boundary, file, handle_response, options); if (isObject(result)) { return result; @@ -572,6 +572,7 @@ function call_api(action, callback, options, get_params) { function post(url, post_data, boundary, file, callback, options) { var file_header = void 0; var finish_buffer = Buffer.from("--" + boundary + "--", 'ascii'); + var oauth_token = options.oauth_token || config().oauth_token; if (file != null || options.stream) { // eslint-disable-next-line no-nested-ternary var filename = options.stream ? options.filename ? options.filename : "file" : basename(file); @@ -588,6 +589,10 @@ function post(url, post_data, boundary, file, callback, options) { if (options.x_unique_upload_id != null) { headers['X-Unique-Upload-Id'] = options.x_unique_upload_id; } + if (oauth_token != null) { + headers.Authorization = `Bearer ${oauth_token}`; + } + post_options = extend(post_options, { method: 'POST', headers: headers diff --git a/lib-es5/utils/index.js b/lib-es5/utils/index.js index edbdb4c9..a9e330fa 100644 --- a/lib-es5/utils/index.js +++ b/lib-es5/utils/index.js @@ -1106,11 +1106,14 @@ function process_request_params(params, options) { if (options.unsigned != null && options.unsigned) { params = exports.clear_blank(params); delete params.timestamp; + } else if (options.oauth_token || config().oauth_token) { + params = exports.clear_blank(options); } else if (options.signature) { params = exports.clear_blank(options); } else { params = exports.sign_request(params, options); } + return params; } diff --git a/lib/api_client/call_api.js b/lib/api_client/call_api.js index 99baac07..5b925ee7 100644 --- a/lib/api_client/call_api.js +++ b/lib/api_client/call_api.js @@ -9,11 +9,17 @@ const { ensurePresenceOf } = utils; function call_api(method, uri, params, callback, options) { ensurePresenceOf({ method, uri }); const api_url = utils.base_api_url(uri, options); - const auth = { - key: ensureOption(options, "api_key"), - secret: ensureOption(options, "api_secret") - }; - + let auth = {}; + if (options.oauth_token || config().oauth_token){ + auth = { + oauth_token: ensureOption(options, "oauth_token") + }; + } else { + auth = { + key: ensureOption(options, "api_key"), + secret: ensureOption(options, "api_secret") + }; + } return execute_request(method, params, auth, api_url, callback, options); } diff --git a/lib/api_client/execute_request.js b/lib/api_client/execute_request.js index 00b1cae9..e88b746f 100644 --- a/lib/api_client/execute_request.js +++ b/lib/api_client/execute_request.js @@ -16,6 +16,7 @@ function execute_request(method, params, auth, api_url, callback, options = {}) let query_params, handle_response; // declare to user later let key = auth.key; let secret = auth.secret; + let oauth_token = auth.oauth_token; let content_type = 'application/x-www-form-urlencoded'; if (options.content_type === 'json') { @@ -36,9 +37,15 @@ function execute_request(method, params, auth, api_url, callback, options = {}) headers: { 'Content-Type': content_type, 'User-Agent': utils.getUserAgent() - }, - auth: key + ":" + secret + } }); + + if (oauth_token) { + request_options.headers.Authorization = `Bearer ${oauth_token}`; + } else { + request_options.auth = key + ":" + secret + } + if (options.agent != null) { request_options.agent = options.agent; } diff --git a/lib/uploader.js b/lib/uploader.js index e68081e2..698db098 100644 --- a/lib/uploader.js +++ b/lib/uploader.js @@ -13,6 +13,7 @@ const https = isSecure ? require('https') : require('http'); const Cache = require('./cache'); const utils = require("./utils"); const UploadStream = require('./upload_stream'); +const config = require("./config"); const { build_upload_params, @@ -459,7 +460,6 @@ function call_api(action, callback, options, get_params) { .map( ([key, value]) => Buffer.from(encodeFieldPart(boundary, key, value), 'utf8') ); - let result = post(api_url, post_data, boundary, file, handle_response, options); if (isObject(result)) { return result; @@ -473,6 +473,7 @@ function call_api(action, callback, options, get_params) { function post(url, post_data, boundary, file, callback, options) { let file_header; let finish_buffer = Buffer.from("--" + boundary + "--", 'ascii'); + let oauth_token = options.oauth_token || config().oauth_token; if ((file != null) || options.stream) { // eslint-disable-next-line no-nested-ternary let filename = options.stream ? options.filename ? options.filename : "file" : basename(file); @@ -489,6 +490,10 @@ function post(url, post_data, boundary, file, callback, options) { if (options.x_unique_upload_id != null) { headers['X-Unique-Upload-Id'] = options.x_unique_upload_id; } + if (oauth_token != null) { + headers.Authorization = `Bearer ${oauth_token}`; + } + post_options = extend(post_options, { method: 'POST', headers: headers diff --git a/lib/utils/index.js b/lib/utils/index.js index 3170346d..882030f4 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1017,11 +1017,14 @@ function process_request_params(params, options) { if ((options.unsigned != null) && options.unsigned) { params = exports.clear_blank(params); delete params.timestamp; + } else if (options.oauth_token || config().oauth_token) { + params = exports.clear_blank(options); } else if (options.signature) { params = exports.clear_blank(options); } else { params = exports.sign_request(params, options); } + return params; } diff --git a/test/integration/api/authorization/oAuth_authorization_spec.js b/test/integration/api/authorization/oAuth_authorization_spec.js new file mode 100644 index 00000000..6cc2283a --- /dev/null +++ b/test/integration/api/authorization/oAuth_authorization_spec.js @@ -0,0 +1,128 @@ +const sinon = require('sinon'); +const cloudinary = require("../../../../cloudinary"); +const helper = require("../../../spechelper"); +const describe = require('../../../testUtils/suite'); +const testConstants = require('../../../testUtils/testConstants'); +const { PUBLIC_IDS } = testConstants; +const { PUBLIC_ID } = PUBLIC_IDS; + +describe("oauth_token", function(){ + it("should send the oauth_token option to the server (admin_api)", function() { + return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => { + cloudinary.v2.api.resource(PUBLIC_ID, { oauth_token: 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4' }); + return sinon.assert.calledWith(requestSpy, + sinon.match.has("headers", + sinon.match.has("Authorization", "Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4") + )); + }); + }); + + it("should send the oauth_token config to the server (admin_api)", function() { + return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => { + cloudinary.config({ + api_key: undefined, + api_secret: undefined, + oauth_token: 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4' + }); + cloudinary.v2.api.resource(PUBLIC_ID); + return sinon.assert.calledWith(requestSpy, + sinon.match.has("headers", + sinon.match.has("Authorization", "Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4") + )); + }); + }); + + it("should not fail when only providing api_key and secret (admin_api)", function() { + cloudinary.config({ + api_key: "1234", + api_secret: "1234", + oauth_token: undefined + }); + return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => { + cloudinary.v2.api.resource(PUBLIC_ID); + return sinon.assert.calledWith(requestSpy, sinon.match({ auth: "1234:1234" })); + }); + }); + + it("should fail when missing all credentials (admin_api)", function() { + cloudinary.config({ + api_key: undefined, + api_secret: undefined, + oauth_token: undefined + }); + expect(() => { + cloudinary.v2.api.resource(PUBLIC_ID) + }).to.throwError(/Must supply api_key/); + }); + + it("oauth_token as option should take priority with secret and key (admin_api)", function() { + cloudinary.config({ + api_key: '1234', + api_secret: '1234' + }); + return cloudinary.v2.api.resource(PUBLIC_ID, {oauth_token: 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4'}) + .then( + () => expect().fail() + ).catch(({ error }) => expect(error.message).to.contain("Invalid token")); + }); + + it("should send the oauth_token option to the server (upload_api)", function() { + return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => { + cloudinary.v2.uploader.upload(PUBLIC_ID, { oauth_token: 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4' }); + return sinon.assert.calledWith(requestSpy, + sinon.match.has("headers", + sinon.match.has("Authorization", "Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4") + )); + }); + }); + + it("should send the oauth_token config to the server (upload_api)", function() { + return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => { + cloudinary.config({ + api_key: undefined, + api_secret: undefined, + oauth_token: 'MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4' + }); + cloudinary.v2.uploader.upload(PUBLIC_ID); + return sinon.assert.calledWith(requestSpy, + sinon.match.has("headers", + sinon.match.has("Authorization", "Bearer MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI4") + )); + }); + }); + + it("should not fail when only providing api_key and secret (upload_api)", function() { + cloudinary.config({ + api_key: "1234", + api_secret: "1234", + oauth_token: undefined + }); + return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => { + cloudinary.v2.uploader.upload(PUBLIC_ID) + return sinon.assert.calledWith(requestSpy, sinon.match({ auth: null })); + }); + }); + + it("should fail when missing all credentials (upload_api)", function() { + cloudinary.config({ + api_key: undefined, + api_secret: undefined, + oauth_token: undefined + }); + expect(() => { + cloudinary.v2.uploader.upload(PUBLIC_ID) + }).to.throwError(/Must supply api_key/); + }); + + it("should not need credentials for unsigned upload", function() { + cloudinary.config({ + api_key: undefined, + api_secret: undefined, + oauth_token: undefined + }); + return helper.provideMockObjects((mockXHR, writeSpy, requestSpy) => { + cloudinary.v2.uploader.unsigned_upload(PUBLIC_ID, 'preset') + return sinon.assert.calledWith(requestSpy, sinon.match({ auth: null })); + }); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index 677339cd..b581f560 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -342,6 +342,7 @@ declare module 'cloudinary' { account_id?: string; provisioning_api_key?: string; provisioning_api_secret?: string; + oauth_token?: string; [futureKey: string]: any; } @@ -387,6 +388,7 @@ declare module 'cloudinary' { export interface AdminApiOptions { agent?: object; content_type?: string; + oauth_token?: string; [futureKey: string]: any; } @@ -509,6 +511,7 @@ declare module 'cloudinary' { use_filename?: boolean; chunk_size?: number; disable_promises?: boolean; + oauth_token?: string; [futureKey: string]: any; }