From 7df5b2235809a2babc475f09bc14591a59cff6b5 Mon Sep 17 00:00:00 2001 From: ankitdas13 Date: Mon, 3 Feb 2025 12:42:51 +0530 Subject: [PATCH 1/6] added aouth implementation --- lib/api.js | 31 +++++++--- lib/oAuthTokenClient.js | 50 ++++++++++++++++ lib/razorpay.js | 12 ++-- test/mocker.js | 3 +- test/oAuthTokenClient.spec.js | 106 ++++++++++++++++++++++++++++++++++ test/razorpay.spec.js | 20 +++---- 6 files changed, 198 insertions(+), 24 deletions(-) create mode 100644 lib/oAuthTokenClient.js create mode 100644 test/oAuthTokenClient.spec.js diff --git a/lib/api.js b/lib/api.js index 8f664a21..dac2d0ce 100644 --- a/lib/api.js +++ b/lib/api.js @@ -40,17 +40,32 @@ function normalizeError(err) { class API { constructor(options) { - this.rq = axios.create({ + this.rq = axios.create(this._createConfig(options)) + } + + _createConfig(options) { + const config = { baseURL: options.hostUrl, - auth: { - username: options.key_id, - password: options.key_secret - }, headers: Object.assign( - {'User-Agent': options.ua}, + { 'User-Agent': options.ua }, getValidHeaders(options.headers) - ) - }) + ), + }; + + if (options.key_id && options.key_secret) { + config.auth = { + username: options.key_id, + password: options.key_secret, + }; + } + + if(options.oauthToken){ + config.headers = { + 'Authorization': `Bearer ${options.oauthToken}`, + ...config.headers + } + } + return config; } version = 'v1'; diff --git a/lib/oAuthTokenClient.js b/lib/oAuthTokenClient.js new file mode 100644 index 00000000..6aee200c --- /dev/null +++ b/lib/oAuthTokenClient.js @@ -0,0 +1,50 @@ +const API = require('./api'); +const pkg = require('../package.json') + +class OAuthTokenClient extends API{ + + constructor(options) { + super({ + hostUrl : 'https://auth.razorpay.com', + ua: `razorpay-node@${pkg.version}`, + ...options + }) + } + + getEntityUrl(params){ + return params.url ; + } + + generateAuthUrl(params) { + const baseUrl = `${this.rq.defaults.baseURL}/authorize`; + const queryString = Object.entries(params) + .map(([key, value]) => key === "redirect_uri" + ? `${key}=${value}` + : `${key}=${encodeURIComponent(value)}`) + .join("&"); + return `${baseUrl}?${queryString}`; + } + + getAccessToken(param = {}, callback){ + return this.post({ + url: '/token', + data: param + }, callback) + } + + refreshToken(param = {}, callback){ + return this.post({ + url: '/token', + data: param + }, callback) + } + + revokeToken(param = {}, callback){ + return this.patch({ + url: '/token', + data: param + }, callback) + } +} + +module.exports = OAuthTokenClient diff --git a/lib/razorpay.js b/lib/razorpay.js index ccfbca09..42ab85c0 100644 --- a/lib/razorpay.js +++ b/lib/razorpay.js @@ -15,21 +15,23 @@ class Razorpay { } constructor(options = {}) { - let { key_id, key_secret, headers } = options + let { key_id, key_secret, oauthToken, headers } = options - if (!key_id) { - throw new Error('`key_id` is mandatory') + if (!key_id && !oauthToken) { + throw new Error('`key_id` or `oauthToken` is mandatory') } this.key_id = key_id this.key_secret = key_secret + this.oauthToken = oauthToken this.api = new API({ hostUrl: 'https://api.razorpay.com', ua: `razorpay-node@${Razorpay.VERSION}`, key_id, key_secret, - headers + headers, + oauthToken }) this.addResources() } @@ -59,7 +61,7 @@ class Razorpay { cards : require('./resources/cards')(this.api), webhooks : require('./resources/webhooks')(this.api), documents : require('./resources/documents')(this.api), - disputes : require('./resources/disputes')(this.api) + disputes : require('./resources/disputes')(this.api) }) } } diff --git a/test/mocker.js b/test/mocker.js index f0e26a96..e131451a 100644 --- a/test/mocker.js +++ b/test/mocker.js @@ -33,12 +33,13 @@ Mocker.prototype.mock = function(params) { if (replyWithError) { return FIXTURES.error } - + let capturedUrl = `${this.req.options.proto}://${this.req.options.hostname}${url}`; return { "success": true, __JUST_FOR_TESTS__: { url, method, + capturedUrl, requestQueryParams, requestBody, headers: this.req.headers diff --git a/test/oAuthTokenClient.spec.js b/test/oAuthTokenClient.spec.js new file mode 100644 index 00000000..09634c0f --- /dev/null +++ b/test/oAuthTokenClient.spec.js @@ -0,0 +1,106 @@ +"use strict"; + +const mocker = require("./mocker"); +const OAuthTokenClient = require("../dist/oAuthTokenClient"); +const { assert } = require("chai"); + +describe("OAuth", () => { + let rzpHost; + let rzpVersion; + const oAuth = new OAuthTokenClient(); + + beforeEach(() => { + rzpHost = mocker.host; + rzpVersion = mocker.version; + mocker.host = "https://auth.razorpay.com"; + mocker.version = ""; + }); + + afterEach(() => { + mocker.host = rzpHost; + mocker.version = rzpVersion; + }); + + it("get access token", (done) => { + let params = { + client_id: "XXXXXXXXXXkQ5C", + client_secret: "XXXXXXXXXXXXXXXXXXHx7rXX", + grant_type: "authorization_code", + redirect_uri: "http://example.com/razorpay_callback", + code: "def50200d844dc80cc44dce2c665d07a374d76802", + mode: "test", + }; + + mocker.mock({ + url: `/token`, + method: "POST", + requestBody: params, + }); + + oAuth + .getAccessToken(params) + .then((response) => { + assert.equal( + response.__JUST_FOR_TESTS__.capturedUrl, + `${mocker.host}/token`, + "Fetch stakeholder url formed correctly" + ); + done(); + }) + .catch((err) => console.log(err)); + }); + + it("refresh access token", (done) => { + let params = { + client_id: "XXXXXXXXXXkQ5C", + client_secret: "XXXXXXXXXXXXXXXXXXHx7rXX", + grant_type: "refresh_token", + refresh_token: "def5020096e1c470c901d34cd60fa53abdaf3662sa0", + }; + + mocker.mock({ + url: `/token`, + method: "POST", + requestBody: params, + }); + + oAuth + .refreshToken(params) + .then((response) => { + assert.equal( + response.__JUST_FOR_TESTS__.capturedUrl, + `${mocker.host}/token`, + "Fetch stakeholder url formed correctly" + ); + done(); + }) + .catch((err) => console.log(err)); + }); + + it("revoke access token", (done) => { + let params = { + client_id: "XXXXXXXXXXkQ5C", + client_secret: "XXXXXXXXXXXXXXXXXXHx7rXX", + grant_type: "refresh_token", + refresh_token: "def5020096e1c470c901d34cd60fa53abdaf3662sa0", + }; + + mocker.mock({ + url: `/token`, + method: "PATCH", + requestBody: params, + }); + + oAuth + .revokeToken(params) + .then((response) => { + assert.equal( + response.__JUST_FOR_TESTS__.capturedUrl, + `${mocker.host}/token`, + "Fetch stakeholder url formed correctly" + ); + done(); + }) + .catch((err) => console.log(err)); + }); +}); diff --git a/test/razorpay.spec.js b/test/razorpay.spec.js index b9091178..48405385 100644 --- a/test/razorpay.spec.js +++ b/test/razorpay.spec.js @@ -5,19 +5,11 @@ const assert = chai.assert const Razorpay = require('../dist/razorpay') describe('Razorpay is initialized properly', () => { - it('Validation for key_id & key_secret', () => { + it('Validation for auth', () => { try { new Razorpay() } catch (e) { - assert.equal(e.message, '`key_id` is mandatory') - } - - try { - new Razorpay({ - key_id: 'XXX' - }) - } catch (e) { - assert.equal(e.message, '`key_secret` is mandatory') + assert.equal(e.message, '`key_id` or `oauthToken` is mandatory') } }) @@ -30,4 +22,12 @@ describe('Razorpay is initialized properly', () => { assert.equal(instance.key_id, 'XXX') assert.equal(instance.key_secret, 'YYY') }) + + it('instance should initialize with oAuth', () => { + let instance = new Razorpay({ + oauthToken: 'XXXXXXXX', + }) + + assert.equal(instance.oauthToken, 'XXXXXXXX') + }) }) From cbec7c67b3f3f1c172117f22fcaba0f831555d84 Mon Sep 17 00:00:00 2001 From: ankitdas13 Date: Tue, 11 Feb 2025 13:03:57 +0530 Subject: [PATCH 2/6] updated types and added validations --- lib/oAuthTokenClient.d.ts | 131 ++++++++++++++++++++++++++++++++++ lib/oAuthTokenClient.js | 68 +++++++++++++++--- lib/razorpay.d.ts | 3 +- lib/types/api.d.ts | 14 ++-- test/oAuthTokenClient.spec.js | 58 +++++++++++---- 5 files changed, 244 insertions(+), 30 deletions(-) create mode 100644 lib/oAuthTokenClient.d.ts diff --git a/lib/oAuthTokenClient.d.ts b/lib/oAuthTokenClient.d.ts new file mode 100644 index 00000000..aeea71df --- /dev/null +++ b/lib/oAuthTokenClient.d.ts @@ -0,0 +1,131 @@ +import API, { INormalizeError } from "./types/api" + +declare namespace OAuthTokenClient { + + interface OAuthTokenBaseRequestBody { + /** + * Unique client identifier. + */ + client_id: string; + /** + * Client secret string. + */ + client_secret: string; + } + + interface InitiateAuthorisationRequest extends Pick{ + /** + * Specifies that the application is requesting an + * authorisation code grant. possible value is `code`. + */ + response_type: string; + /** + * Callback URL used by Razorpay to redirect after the user approves or denies the authorisation request. + * The client should whitelist the `redirect_uri`. + */ + redirect_uri: string; + /** + * Defines what access your application is requesting from the user. You can request multiple scopes + * by separating with a space. + * possible values is `read_only` or `read_write`. + */ + scope: string; + /** + * Check [doc](https://razorpay.com/docs/partners/technology-partners/onboard-businesses/integrate-oauth/integration-steps/#query-parameters) for required params + */ + state: string; + } + + interface OAuthTokenTokenRequest extends OAuthTokenBaseRequestBody { + /** + * Defines the grant type for the request. possible value is `authorization_code` + */ + grant_type: string; + /** + * Specifies the same `redirect_uri` used in the authorisation request. + */ + redirect_uri?: string; + /** + * Decoded authorisation code received in the last step. + */ + code?: string; + /** + * The type of mode. possible values is `test` or `live`. + */ + mode?: string; + /** + * Used to refresh the access token when it expires. + */ + refresh_token?: string; + /** + * The type of token for the request. possible value is `access_token` or `access_token`. + */ + token_type_hint?: string; + /** + * The token whose access should be revoked. + */ + token?: string; + } + + interface OAuthTokenTokenResponse { + /** + * A public key is used only for public routes such as Checkout or Payments. + */ + public_token: string; + /** + * Defines the type of access token. possible value is `Bearer` + */ + token_type: string; + /** + * Integer representing the TTL of the access token in seconds. + */ + expires_in: number; + /** + * A private key used to access sub-merchant resources on Razorpay. + * used for server-to-server calls only. + */ + access_token: string; + /** + * Used to refresh the access token when it expires. + */ + refresh_token:string; + /** + * Identifies the sub-merchant ID who granted the authorisation. + */ + razorpay_account_id: string; + } +} + +declare class OAuthTokenClient extends API{ + constructor() + + getEntityUrl(): string; + /** + * Initiate Authorisation Using URL + * @param param - Check [doc](https://razorpay.com/docs/partners/technology-partners/onboard-businesses/integrate-oauth/integration-steps/#query-parameters) for required params + */ + generateAuthUrl(param: OAuthTokenClient.InitiateAuthorisationRequest): string; + + /** + * Get access token + * @param param - Check [doc](https://razorpay.com/docs/partners/technology-partners/onboard-businesses/integrate-oauth/integration-steps/#request-parameters) for required params + */ + getAccessToken(param: OAuthTokenClient.OAuthTokenTokenRequest): Promise; + getAccessToken(param: OAuthTokenClient.OAuthTokenTokenRequest, callback: (err: INormalizeError | null, data: OAuthTokenClient.OAuthTokenTokenResponse) => void): void + + /** + * Get refresh token + * @param param - Check [doc](https://razorpay.com/docs/partners/technology-partners/onboard-businesses/integrate-oauth/integration-steps/#refresh-token-api) for required params + */ + refreshToken(param: OAuthTokenClient.OAuthTokenTokenRequest): Promise; + refreshToken(param: OAuthTokenClient.OAuthTokenTokenRequest, callback: (err: INormalizeError | null, data: OAuthTokenClient.OAuthTokenTokenResponse) => void): void + + /** + * Revoke token + * @param param - Check [doc](https://razorpay.com/docs/partners/technology-partners/onboard-businesses/integrate-oauth/integration-steps/#revoke-token-api) for required params + */ + revokeToken(param: OAuthTokenClient.OAuthTokenTokenRequest): Promise<{ message: string;}>; + revokeToken(param: OAuthTokenClient.OAuthTokenTokenRequest, callback: (err: INormalizeError | null, data: { message: string;}) => void): void +} + +export = OAuthTokenClient diff --git a/lib/oAuthTokenClient.js b/lib/oAuthTokenClient.js index 6aee200c..05b44701 100644 --- a/lib/oAuthTokenClient.js +++ b/lib/oAuthTokenClient.js @@ -1,13 +1,41 @@ const API = require('./api'); const pkg = require('../package.json') +const SCHEMAS = { + generateAuthUrl: { + client_id: "client_id is empty", + response_type: "response_type is empty", + redirect_uri: "redirect_uri is empty", + scope: "scope is empty", + state: "state is empty", + }, + getAccessToken: { + client_id: "client_id is empty", + client_secret: "client_secret is empty", + grant_type: "grant_type is empty", + redirect_uri: "redirect_uri is empty", + code: "code is empty", + }, + refreshToken: { + client_id: "client_id is empty", + client_secret: "client_secret is empty", + grant_type: "grant_type is empty", + refresh_token: "refresh_token is empty", + }, + revokeToken: { + client_id: "client_id is empty", + client_secret: "client_secret is empty", + token_type_hint: "token_type_hint is empty", + token: "token is empty", + }, +}; + class OAuthTokenClient extends API{ - constructor(options) { + constructor() { super({ hostUrl : 'https://auth.razorpay.com', ua: `razorpay-node@${pkg.version}`, - ...options }) } @@ -15,7 +43,9 @@ class OAuthTokenClient extends API{ return params.url ; } - generateAuthUrl(params) { + generateAuthUrl(params) { + const errors = this.validateInput(params, SCHEMAS.generateAuthUrl); + if (Object.keys(errors).length > 0) return errors; const baseUrl = `${this.rq.defaults.baseURL}/authorize`; const queryString = Object.entries(params) .map(([key, value]) => key === "redirect_uri" @@ -25,26 +55,42 @@ class OAuthTokenClient extends API{ return `${baseUrl}?${queryString}`; } - getAccessToken(param = {}, callback){ + getAccessToken(params = {}, callback){ + const errors = this.validateInput(params, SCHEMAS.getAccessToken); + if (Object.keys(errors).length > 0) return Promise.reject(errors); return this.post({ url: '/token', - data: param + data: params }, callback) } - refreshToken(param = {}, callback){ + refreshToken(params = {}, callback){ + const errors = this.validateInput(params, SCHEMAS.refreshToken); + if (Object.keys(errors).length > 0) return Promise.reject(errors); return this.post({ url: '/token', - data: param + data: params }, callback) } - revokeToken(param = {}, callback){ - return this.patch({ - url: '/token', - data: param + revokeToken(params = {}, callback){ + const errors = this.validateInput(params, SCHEMAS.revokeToken); + if (Object.keys(errors).length > 0) return Promise.reject(errors); + return this.post({ + url: '/revoke', + data: params }, callback) } + + validateInput(inputData, schema) { + let errors = {}; + for (let field in schema) { + if (!(field in inputData) || inputData[field].trim() === "") { + errors[field] = schema[field]; + } + } + return errors; + } } module.exports = OAuthTokenClient diff --git a/lib/razorpay.d.ts b/lib/razorpay.d.ts index f0e5ed1b..3d65e6c6 100644 --- a/lib/razorpay.d.ts +++ b/lib/razorpay.d.ts @@ -26,9 +26,10 @@ import documents from './types/documents' import disputes from './types/disputes' interface IRazorpayConfig { - key_id: string; + key_id?: string; key_secret?: string; headers?: RazorpayHeaders; + oauthToken?: string; } declare class Razorpay { diff --git a/lib/types/api.d.ts b/lib/types/api.d.ts index 5530f2fc..813ebb4e 100644 --- a/lib/types/api.d.ts +++ b/lib/types/api.d.ts @@ -1,11 +1,13 @@ import nodeify from '../utils/nodeify' -interface IOption { - hostUrl: string; - key_id: string; - key_secret?: string; - ua: string; - headers?: string; + +interface IOption { + key_id?: string; + key_secret?: string; + headers?: RazorpayHeaders; + oauthToken?: string; + hostUrl: string; + ua: string; } interface IPayload { diff --git a/test/oAuthTokenClient.spec.js b/test/oAuthTokenClient.spec.js index 09634c0f..e13fa8fc 100644 --- a/test/oAuthTokenClient.spec.js +++ b/test/oAuthTokenClient.spec.js @@ -1,8 +1,9 @@ "use strict"; const mocker = require("./mocker"); -const OAuthTokenClient = require("../dist/oAuthTokenClient"); +const OAuthTokenClient = require("../lib/oAuthTokenClient"); const { assert } = require("chai"); +const { error } = require("console"); describe("OAuth", () => { let rzpHost; @@ -43,11 +44,46 @@ describe("OAuth", () => { assert.equal( response.__JUST_FOR_TESTS__.capturedUrl, `${mocker.host}/token`, - "Fetch stakeholder url formed correctly" + "Access token url formed correctly" ); done(); }) - .catch((err) => console.log(err)); + }); + + it("get access token with invalid param", (done) => { + let params = { + client_id: "XXXXXXXXXXkQ5C", + client_secret: "XXXXXXXXXXXXXXXXXXHx7rXX", + grant_type: "authorization_code", + }; + + mocker.mock({ + url: `/token`, + method: "POST", + requestBody: params, + }); + + oAuth + .getAccessToken(params) + .catch(err=>{ + console.log('getAccessToken', err) + assert.hasAllKeys(err, ['redirect_uri', 'code']); + done(); + }) + }); + + it("refresh access token with invalid param", (done) => { + mocker.mock({ + url: `/token`, + method: "POST", + }); + + oAuth + .refreshToken() + .catch((err) => { + assert.hasAnyKeys(err, ['client_id']) + done(); + }) }); it("refresh access token", (done) => { @@ -70,24 +106,23 @@ describe("OAuth", () => { assert.equal( response.__JUST_FOR_TESTS__.capturedUrl, `${mocker.host}/token`, - "Fetch stakeholder url formed correctly" + "Refresh access token url formed correctly" ); done(); }) - .catch((err) => console.log(err)); }); it("revoke access token", (done) => { let params = { client_id: "XXXXXXXXXXkQ5C", client_secret: "XXXXXXXXXXXXXXXXXXHx7rXX", - grant_type: "refresh_token", - refresh_token: "def5020096e1c470c901d34cd60fa53abdaf3662sa0", + token_type_hint: "access_token", + token: "def5020096e1c470c901d34cd60fa53abdaf3662sa0", }; mocker.mock({ - url: `/token`, - method: "PATCH", + url: `/revoke`, + method: "POST", requestBody: params, }); @@ -96,11 +131,10 @@ describe("OAuth", () => { .then((response) => { assert.equal( response.__JUST_FOR_TESTS__.capturedUrl, - `${mocker.host}/token`, - "Fetch stakeholder url formed correctly" + `${mocker.host}/revoke`, + "Revoke access token url formed correctly" ); done(); }) - .catch((err) => console.log(err)); }); }); From 057b4250973cda07090c6e21c399e84ba89c9c3d Mon Sep 17 00:00:00 2001 From: ankitdas13 Date: Fri, 14 Feb 2025 10:59:22 +0530 Subject: [PATCH 3/6] update types --- lib/oAuthTokenClient.d.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/oAuthTokenClient.d.ts b/lib/oAuthTokenClient.d.ts index aeea71df..e49ccd19 100644 --- a/lib/oAuthTokenClient.d.ts +++ b/lib/oAuthTokenClient.d.ts @@ -36,11 +36,11 @@ declare namespace OAuthTokenClient { state: string; } - interface OAuthTokenTokenRequest extends OAuthTokenBaseRequestBody { + interface OAuthTokenRequest extends OAuthTokenBaseRequestBody { /** * Defines the grant type for the request. possible value is `authorization_code` */ - grant_type: string; + grant_type?: string; /** * Specifies the same `redirect_uri` used in the authorisation request. */ @@ -58,7 +58,7 @@ declare namespace OAuthTokenClient { */ refresh_token?: string; /** - * The type of token for the request. possible value is `access_token` or `access_token`. + * The type of token for the request. possible value is `access_token` or `refresh_token`. */ token_type_hint?: string; /** @@ -110,22 +110,22 @@ declare class OAuthTokenClient extends API{ * Get access token * @param param - Check [doc](https://razorpay.com/docs/partners/technology-partners/onboard-businesses/integrate-oauth/integration-steps/#request-parameters) for required params */ - getAccessToken(param: OAuthTokenClient.OAuthTokenTokenRequest): Promise; - getAccessToken(param: OAuthTokenClient.OAuthTokenTokenRequest, callback: (err: INormalizeError | null, data: OAuthTokenClient.OAuthTokenTokenResponse) => void): void + getAccessToken(param: OAuthTokenClient.OAuthTokenRequest): Promise; + getAccessToken(param: OAuthTokenClient.OAuthTokenRequest, callback: (err: INormalizeError | null, data: OAuthTokenClient.OAuthTokenTokenResponse) => void): void /** * Get refresh token * @param param - Check [doc](https://razorpay.com/docs/partners/technology-partners/onboard-businesses/integrate-oauth/integration-steps/#refresh-token-api) for required params */ - refreshToken(param: OAuthTokenClient.OAuthTokenTokenRequest): Promise; - refreshToken(param: OAuthTokenClient.OAuthTokenTokenRequest, callback: (err: INormalizeError | null, data: OAuthTokenClient.OAuthTokenTokenResponse) => void): void + refreshToken(param: OAuthTokenClient.OAuthTokenRequest): Promise; + refreshToken(param: OAuthTokenClient.OAuthTokenRequest, callback: (err: INormalizeError | null, data: OAuthTokenClient.OAuthTokenTokenResponse) => void): void /** * Revoke token * @param param - Check [doc](https://razorpay.com/docs/partners/technology-partners/onboard-businesses/integrate-oauth/integration-steps/#revoke-token-api) for required params */ - revokeToken(param: OAuthTokenClient.OAuthTokenTokenRequest): Promise<{ message: string;}>; - revokeToken(param: OAuthTokenClient.OAuthTokenTokenRequest, callback: (err: INormalizeError | null, data: { message: string;}) => void): void + revokeToken(param: OAuthTokenClient.OAuthTokenRequest): Promise<{ message: string;}>; + revokeToken(param: OAuthTokenClient.OAuthTokenRequest, callback: (err: INormalizeError | null, data: { message: string;}) => void): void } export = OAuthTokenClient From d135a00f2277767c8dc41511ca861ec2e361f339 Mon Sep 17 00:00:00 2001 From: ankitdas13 Date: Mon, 17 Feb 2025 18:14:26 +0530 Subject: [PATCH 4/6] validation and documention update --- documents/oAuthTokenClient.md | 148 +++++++++++++++++++++++++++ lib/oAuthTokenClient.d.ts | 11 +- lib/oAuthTokenClient.js | 60 +++-------- lib/resources/oAuthTokenValidator.js | 51 +++++++++ lib/utils/razorpay-utils.d.ts | 4 +- lib/utils/razorpay-utils.js | 47 ++++++++- 6 files changed, 267 insertions(+), 54 deletions(-) create mode 100644 documents/oAuthTokenClient.md create mode 100644 lib/resources/oAuthTokenValidator.js diff --git a/documents/oAuthTokenClient.md b/documents/oAuthTokenClient.md new file mode 100644 index 00000000..6a411691 --- /dev/null +++ b/documents/oAuthTokenClient.md @@ -0,0 +1,148 @@ +## OAuth Token Client + +### Generate Authorize Url +```js + +const OAuthTokenClient = require("./dist/oAuthTokenClient"); +const { generateOnboardingSignature } = require("./dist/utils/razorpay-utils") + +// Initialize client +let oAuth = new OAuthTokenClient(); + +let attributes = { + "submerchant_id": "", + "timestamp": Math.floor(Date.now() / 1000) +} + +let onboarding_signature = generateOnboardingSignature(attributes, ""); + +// Not an promise +const authUrl = oAuth.generateAuthUrl({ + "client_id": "", + "response_type": "code", + "redirect_uri": "https://example.com/razorpay_callback", + "scope": ["read_write"], + "state": "NOBYtv8r6c75ex6WZ", + "onboarding_signature": onboarding_signature +}); + +console.log(authUrl) +``` + +**Parameters:** + +| Name | Type | Description | +|----------------------|--|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| client_id* | string | Unique client identifier. | +| redirect_uri* | string | Callback URL used by Razorpay to redirect after the user approves or denies the authorisation request. The client should whitelist the 'redirect_uri'. | +| scopes* | array | Defines what access your application is requesting from the user. You can request one or multiple scopes by adding them to an array as indicated above. | +| state* | string | A random string generated by your service. This parameter helps prevent cross-site request forgery (CSRF) attacks. | +| onboarding_signature | string | A cryptographic string generated by your service using generateOnboardingSignature method in Utils class. Only applicable for accounts created with pre-fill KYC | + +**Response:** +``` +"https://auth.razorpay.com/authorize?response_type=code&client_id=&redirect_uri=https:%2F%2Fexample.com%2Frazorpay_callback&scope[]=read_only&scope[]=rx_read_write&state=NOBYtv8r6c75ex6WZ&onboarding_signature=" +``` + +------------------------------------------------------------------------------------------------------- + +### Get Access token +```js +oAuth.getAccessToken({ + "client_id": "", + "client_secret": "", + "grant_type": "authorization_code", + "redirect_uri": "https://example.com", + "code": "def50200d844dc80cc44dce2c665d07a374d76802", + "mode": "test" +}) +``` + +**Parameters:** + +| Name | Type | Description | +|----------------|--------|------------------------------------------------------------------------------------------------------------------------------| +| client_id* | string | Unique client identifier. | +| client_secret* | string | Client secret string. | +| redirect_uri* | string | Specifies the same redirect_uri used in the authorisation request. | +| grant_type* | string | Defines the grant type for the request. Possible value are:
  • authorization_code
  • client_credentials
| +| code* | string | Decoded authorisation code received in the last step. Note: Pass this parameter only when grant_type is 'authorization_code' | +| mode | string | The type of mode. Possible values:
  • test
  • live (default)
| + +**Response:** +```json +{ + "public_token": "rzp_test_oauth_9xu1rkZqoXlClS", + "token_type": "Bearer", + "expires_in": 7862400, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IkY1Z0NQYkhhRzRjcUpnIn0.eyJhdWQiOiJGNFNNeEgxanMxbkpPZiIsImp0aSI6IkY1Z0NQYkhhRzRjcUpnIiwiaWF0IjoxNTkyODMxMDExLCJuYmYiOjE1OTI4MzEwMTEsInN1YiI6IiIsImV4cCI6MTYwMDc3OTgxMSwidXNlcl9pZCI6IkYycVBpejJEdzRPRVFwIiwibWVyY2hhbnRfaWQiOiJGMnFQaVZ3N0lNV01GSyIsInNjb3BlcyI6WyJyZWFkX29ubHkiXX0.Wwqt5czhoWpVzP5_aoiymKXoGj-ydo-4A_X2jf_7rrSvk4pXdqzbA5BMrHxPdPbeFQWV6vsnsgbf99Q3g-W4kalHyH67LfAzc3qnJ-mkYDkFY93tkeG-MCco6GJW-Jm8xhaV9EPUak7z9J9jcdluu9rNXYMtd5qxD8auyRYhEgs", + "refresh_token": "def50200f42e07aded65a323f6c53181d802cc797b62cc5e78dd8038d6dff253e5877da9ad32f463a4da0ad895e3de298cbce40e162202170e763754122a6cb97910a1f58e2378ee3492dc295e1525009cccc45635308cce8575bdf373606c453ebb5eb2bec062ca197ac23810cf9d6cf31fbb9fcf5b7d4de9bf524c89a4aa90599b0151c9e4e2fa08acb6d2fe17f30a6cfecdfd671f090787e821f844e5d36f5eacb7dfb33d91e83b18216ad0ebeba2bef7721e10d436c3984daafd8654ed881c581d6be0bdc9ebfaee0dc5f9374d7184d60aae5aa85385690220690e21bc93209fb8a8cc25a6abf1108d8277f7c3d38217b47744d7", + "razorpay_account_id": "acc_Dhk2qDbmu6FwZH" +} +``` +------------------------------------------------------------------------------------------------------- + +### Get Access token using refresh token +```js +oAuth.refreshToken({ + "client_id": "", + "client_secret": "", + "grant_type": "authorization_code", + "refresh_token": "def50200d844dc80cc44dce2c665d07a374d76802" +}) +``` + +**Parameters:** + +| Name | Type | Description | +|----------------|-----------|--------------------------------------------| +| client_id* | string | Unique client identifier. | +| client_secret* | string | Client secret string. | +| grant_type* | string | Defines the grant type for the request. Possible value are:
  • authorization_code
  • client_credentials
| +| refresh_token* | string | The previously-stored refresh token value. | + + +**Response:** +```json +{ + "public_token": "rzp_test_oauth_9xu1rkZqoXlClS", + "token_type": "Bearer", + "expires_in": 7862400, + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6Ijl4dTF", + "refresh_token": "def5020096e1c470c901d34cd60fa53abdaf36620e823ffa53" +} +``` + +------------------------------------------------------------------------------------------------------- + +### Revoke a token +```js +oAuth.revokeToken({ + "client_id": "", + "client_secret": "", + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6Ijl4dTF", + "token_type_hint": "access_token" +}) +``` + +**Parameters:** + +| Name | Type | Description | +|------------------|----------|----------------------------------------------------------------------------------------------------------| +| client_id* | string | Unique client identifier. | +| client_secret* | string | Client secret string. | +| token_type_hint* | string | The type of token for the request. Possible values:
  • access_token
  • refresh_token
| +| token* | string | The token whose access should be revoked. | + +**Response:** +```json +{ + "message": "Token Revoked" +} +``` +------------------------------------------------------------------------------------------------------- + +**PN: * indicates mandatory fields** +
+
+**For reference click [here](https://razorpay.com/docs/partners/platform/onboard-businesses/integrate-oauth/integration-steps)** \ No newline at end of file diff --git a/lib/oAuthTokenClient.d.ts b/lib/oAuthTokenClient.d.ts index e49ccd19..b4ee31c3 100644 --- a/lib/oAuthTokenClient.d.ts +++ b/lib/oAuthTokenClient.d.ts @@ -29,18 +29,19 @@ declare namespace OAuthTokenClient { * by separating with a space. * possible values is `read_only` or `read_write`. */ - scope: string; + scope: string | string[]; /** * Check [doc](https://razorpay.com/docs/partners/technology-partners/onboard-businesses/integrate-oauth/integration-steps/#query-parameters) for required params */ state: string; + onboarding_signature?: string; } interface OAuthTokenRequest extends OAuthTokenBaseRequestBody { /** - * Defines the grant type for the request. possible value is `authorization_code` + * Defines the grant type for the request. possible value is `authorization_code` or `refresh_token` */ - grant_type?: string; + grant_type?: "authorization_code" | "refresh_token"; /** * Specifies the same `redirect_uri` used in the authorisation request. */ @@ -52,7 +53,7 @@ declare namespace OAuthTokenClient { /** * The type of mode. possible values is `test` or `live`. */ - mode?: string; + mode?: "test" | "live"; /** * Used to refresh the access token when it expires. */ @@ -60,7 +61,7 @@ declare namespace OAuthTokenClient { /** * The type of token for the request. possible value is `access_token` or `refresh_token`. */ - token_type_hint?: string; + token_type_hint?: "access_token" | "refresh_token"; /** * The token whose access should be revoked. */ diff --git a/lib/oAuthTokenClient.js b/lib/oAuthTokenClient.js index 05b44701..234cca59 100644 --- a/lib/oAuthTokenClient.js +++ b/lib/oAuthTokenClient.js @@ -1,34 +1,6 @@ const API = require('./api'); -const pkg = require('../package.json') - -const SCHEMAS = { - generateAuthUrl: { - client_id: "client_id is empty", - response_type: "response_type is empty", - redirect_uri: "redirect_uri is empty", - scope: "scope is empty", - state: "state is empty", - }, - getAccessToken: { - client_id: "client_id is empty", - client_secret: "client_secret is empty", - grant_type: "grant_type is empty", - redirect_uri: "redirect_uri is empty", - code: "code is empty", - }, - refreshToken: { - client_id: "client_id is empty", - client_secret: "client_secret is empty", - grant_type: "grant_type is empty", - refresh_token: "refresh_token is empty", - }, - revokeToken: { - client_id: "client_id is empty", - client_secret: "client_secret is empty", - token_type_hint: "token_type_hint is empty", - token: "token is empty", - }, -}; +const pkg = require('../package.json'); +const { validateInput, SCHEMAS } = require('./resources/oAuthTokenValidator'); class OAuthTokenClient extends API{ @@ -44,19 +16,23 @@ class OAuthTokenClient extends API{ } generateAuthUrl(params) { - const errors = this.validateInput(params, SCHEMAS.generateAuthUrl); + const errors = validateInput(params, SCHEMAS.generateAuthUrl); if (Object.keys(errors).length > 0) return errors; const baseUrl = `${this.rq.defaults.baseURL}/authorize`; const queryString = Object.entries(params) - .map(([key, value]) => key === "redirect_uri" - ? `${key}=${value}` - : `${key}=${encodeURIComponent(value)}`) + .flatMap(([key, value]) => + Array.isArray(value) + ? value.map(item => `${key}[]=${encodeURIComponent(item)}`) + : key === "redirect_uri" + ? `${key}=${value}` + : `${key}=${encodeURIComponent(value)}` + ) .join("&"); return `${baseUrl}?${queryString}`; } getAccessToken(params = {}, callback){ - const errors = this.validateInput(params, SCHEMAS.getAccessToken); + const errors = validateInput(params, SCHEMAS.getAccessToken); if (Object.keys(errors).length > 0) return Promise.reject(errors); return this.post({ url: '/token', @@ -65,7 +41,7 @@ class OAuthTokenClient extends API{ } refreshToken(params = {}, callback){ - const errors = this.validateInput(params, SCHEMAS.refreshToken); + const errors = validateInput(params, SCHEMAS.refreshToken); if (Object.keys(errors).length > 0) return Promise.reject(errors); return this.post({ url: '/token', @@ -74,23 +50,13 @@ class OAuthTokenClient extends API{ } revokeToken(params = {}, callback){ - const errors = this.validateInput(params, SCHEMAS.revokeToken); + const errors = validateInput(params, SCHEMAS.revokeToken); if (Object.keys(errors).length > 0) return Promise.reject(errors); return this.post({ url: '/revoke', data: params }, callback) } - - validateInput(inputData, schema) { - let errors = {}; - for (let field in schema) { - if (!(field in inputData) || inputData[field].trim() === "") { - errors[field] = schema[field]; - } - } - return errors; - } } module.exports = OAuthTokenClient diff --git a/lib/resources/oAuthTokenValidator.js b/lib/resources/oAuthTokenValidator.js new file mode 100644 index 00000000..20402648 --- /dev/null +++ b/lib/resources/oAuthTokenValidator.js @@ -0,0 +1,51 @@ +const { isValidUrl } = require("../utils/razorpay-utils"); + +const SCHEMAS = { + generateAuthUrl: { + client_id: "client_id is empty", + response_type: "response_type is empty", + redirect_uri: "redirect_uri is empty", + scope: "scope is empty", + state: "state is empty", + }, + getAccessToken: { + client_id: "client_id is empty", + client_secret: "client_secret is empty", + grant_type: "grant_type is empty", + redirect_uri: "redirect_uri is empty", + code: "code is empty", + }, + refreshToken: { + client_id: "client_id is empty", + client_secret: "client_secret is empty", + grant_type: "grant_type is empty", + refresh_token: "refresh_token is empty", + }, + revokeToken: { + client_id: "client_id is empty", + client_secret: "client_secret is empty", + token_type_hint: "token_type_hint is empty", + token: "token is empty", + }, + }; + + function validateInput(inputData, schema) { + let errors = {}; + for (let field in schema) { + if (!(field in inputData) || (typeof inputData[field] === "string" && inputData[field].trim() === "")) { + errors[field] = schema[field]; + } else if (field === "redirect_uri" && ! isValidUrl(inputData[field])) { + errors[field] = "redirect_uri is not a valid URL"; + } else if (field === "client_id" && !/^[A-Za-z0-9]{1,14}$/.test(inputData[field])) { + errors[field] = "client_id is not a valid ID"; + } else if(field === "grant_type" && (inputData[field] !== "refresh_token" && inputData[field] !== "authorization_code")){ + errors[field] = "grant_type is not a valid"; + } + } + return errors; + } + +module.exports = { + SCHEMAS, + validateInput +} \ No newline at end of file diff --git a/lib/utils/razorpay-utils.d.ts b/lib/utils/razorpay-utils.d.ts index 5acfcafd..f584875c 100644 --- a/lib/utils/razorpay-utils.d.ts +++ b/lib/utils/razorpay-utils.d.ts @@ -85,4 +85,6 @@ export function validatePaymentVerification(payload: RazorpayVerifyPayment | Raz * * @param val */ -export function prettify(val: Object): string \ No newline at end of file +export function prettify(val: Object): string + +export function generateOnboardingSignature(params: any, secret: string): string \ No newline at end of file diff --git a/lib/utils/razorpay-utils.js b/lib/utils/razorpay-utils.js index 2af47d78..6ba64689 100644 --- a/lib/utils/razorpay-utils.js +++ b/lib/utils/razorpay-utils.js @@ -1,3 +1,5 @@ +const crypto = require("crypto"); + function getDateInSecs(date) { return (+new Date(date))/1000 } @@ -142,6 +144,47 @@ function validatePaymentVerification(params={}, signature, secret){ return validateWebhookSignature(payload,signature,secret); }; +function generateOnboardingSignature(params={}, secret){ + let jsonStr = JSON.stringify(params); + return encrypt(jsonStr, secret); +} + +function encrypt(dataToEncrypt, secret) { + try { + // Use first 16 bytes of secret as key + const keyBytes = Buffer.from(secret.slice(0, 16), 'utf8'); + + // Use first 12 bytes of key as IV + const iv = Buffer.alloc(12); + keyBytes.copy(iv, 0, 0, 12); + + // Create cipher with AES-GCM + const cipher = crypto.createCipheriv('aes-128-gcm', keyBytes, iv); + + // Encrypt the data + let encryptedData = cipher.update(dataToEncrypt, 'utf8'); + encryptedData = Buffer.concat([encryptedData, cipher.final()]); + + // Get authentication tag and append to encrypted data + const authTag = cipher.getAuthTag(); + const finalData = Buffer.concat([encryptedData, authTag]); + + // Convert to hex string + return finalData.toString('hex'); + } catch (err) { + throw new Error(`Encryption failed: ${err.message}`); + } +} + +function isValidUrl(url) { + try { + new URL(url); + return true; + } catch (error) { + return false; + } +} + module.exports = { normalizeNotes, normalizeDate, @@ -153,5 +196,7 @@ module.exports = { isNonNullObject, getTestError, validateWebhookSignature, - validatePaymentVerification + validatePaymentVerification, + isValidUrl, + generateOnboardingSignature } From 53d27993c92ef9357389177b7a7508603db4f888 Mon Sep 17 00:00:00 2001 From: ankitdas13 Date: Tue, 18 Feb 2025 13:53:47 +0530 Subject: [PATCH 5/6] remove console --- test/oAuthTokenClient.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/oAuthTokenClient.spec.js b/test/oAuthTokenClient.spec.js index e13fa8fc..80af0a8b 100644 --- a/test/oAuthTokenClient.spec.js +++ b/test/oAuthTokenClient.spec.js @@ -3,7 +3,6 @@ const mocker = require("./mocker"); const OAuthTokenClient = require("../lib/oAuthTokenClient"); const { assert } = require("chai"); -const { error } = require("console"); describe("OAuth", () => { let rzpHost; @@ -66,7 +65,6 @@ describe("OAuth", () => { oAuth .getAccessToken(params) .catch(err=>{ - console.log('getAccessToken', err) assert.hasAllKeys(err, ['redirect_uri', 'code']); done(); }) From ab6c4b1c29e567191cd79a5ae3fc18048356b1fd Mon Sep 17 00:00:00 2001 From: ankitdas13 Date: Thu, 20 Feb 2025 20:00:57 +0530 Subject: [PATCH 6/6] updated package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aafd44a4..53704f5e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "prepublish": "npm test", "clean": "rm -rf dist && mkdir dist", "cp-types": "mkdir dist/types && cp lib/types/* dist/types && cp lib/utils/*d.ts dist/utils", - "cp-ts": "cp lib/razorpay.d.ts dist/ && npm run cp-types", + "cp-ts": "cp lib/razorpay.d.ts dist/ && cp lib/oAuthTokenClient.d.ts dist/ && npm run cp-types", "build:commonjs": "babel lib -d dist", "build": "npm run clean && npm run build:commonjs && npm run cp-ts", "debug": "npm run build && node-debug examples/index.js",