Skip to content

Commit

Permalink
feat: add support for OAuth and Orgs API (#1057)
Browse files Browse the repository at this point in the history
* chore: add support for oauth in public apis (#1050)

* chore: Add tests for node public oauth (#1053)

* chore: add cluster tests for orgs api (#1054)

* chore: add cluster tests for orgs api

* chore: add documentation for orgs API and public oauth (#1056)
  • Loading branch information
tiwarishubham635 authored Dec 11, 2024
1 parent 043d3a1 commit 10a9474
Show file tree
Hide file tree
Showing 40 changed files with 3,685 additions and 20 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/test-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ jobs:
TWILIO_API_SECRET: ${{ secrets.TWILIO_CLUSTER_TEST_API_KEY_SECRET }}
TWILIO_FROM_NUMBER: ${{ secrets.TWILIO_FROM_NUMBER }}
TWILIO_TO_NUMBER: ${{ secrets.TWILIO_TO_NUMBER }}
TWILIO_ORGS_CLIENT_ID: ${{ secrets.TWILIO_ORGS_CLIENT_ID }}
TWILIO_ORGS_CLIENT_SECRET: ${{ secrets.TWILIO_ORGS_CLIENT_SECRET }}
TWILIO_ORGS_USER_ID: ${{ secrets.TWILIO_ORGS_USER_ID }}
TWILIO_ORG_SID: ${{ secrets.TWILIO_ORG_SID }}
TWILIO_CLIENT_ID: ${{ secrets.TWILIO_CLIENT_ID }}
TWILIO_CLIENT_SECRET: ${{ secrets.TWILIO_CLIENT_SECRET }}
TWILIO_MESSAGE_SID: ${{ secrets.TWILIO_MESSAGE_SID }}
run: |
npm pack
tar -xzf twilio*.tgz
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ After a brief delay, you will receive the text message on your phone.
> **Warning**
> It's okay to hardcode your credentials when testing locally, but you should use environment variables to keep them secret before committing any code or deploying to production. Check out [How to Set Environment Variables](https://www.twilio.com/blog/2017/01/how-to-set-environment-variables.html) for more information.
## OAuth Feature for Twilio APIs
We are introducing Client Credentials Flow-based OAuth 2.0 authentication. This feature is currently in beta and its implementation is subject to change.

API examples [here](https://github.com/twilio/twilio-node/blob/main/examples/public_oauth.js)

Organisation API examples [here](https://github.com/twilio/twilio-node/blob/main/examples/orgs_api.js)

## Usage

Check out these [code examples](examples) in JavaScript and TypeScript to get up and running quickly.
Expand Down
37 changes: 37 additions & 0 deletions examples/orgs_api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use strict";
var Twilio = require("../lib");

const clientId = process.env.ORGS_CLIENT_ID;
const clientSecret = process.env.ORGS_CLIENT_SECRET;
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const organizationSid = process.env.TWILIO_ORG_SID;

const orgsCredentialProvider = new Twilio.OrgsCredentialProviderBuilder()
.setClientId(clientId)
.setClientSecret(clientSecret)
.build();

const client = new Twilio();
client.setCredentialProvider(orgsCredentialProvider);
client.setAccountSid(accountSid);

client.previewIam
.organization(organizationSid)
.accounts.list()
.then((accounts) => {
console.log(accounts);
})
.catch((error) => {
console.log(error);
});

client.previewIam
.organization(organizationSid)
.accounts(accountSid)
.fetch()
.then((account) => {
console.log(account);
})
.catch((error) => {
console.log(error);
});
25 changes: 25 additions & 0 deletions examples/public_oauth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
var Twilio = require("../lib");

const clientId = process.env.OAUTH_CLIENT_ID;
const clientSecret = process.env.OAUTH_CLIENT_SECRET;
const accountSid = process.env.TWILIO_ACCOUNT_SID;

const clientCredentialProvider = new Twilio.ClientCredentialProviderBuilder()
.setClientId(clientId)
.setClientSecret(clientSecret)
.build();

const client = new Twilio();
client.setCredentialProvider(clientCredentialProvider);
client.setAccountSid(accountSid);

const messageId = "SMXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
client
.messages(messageId)
.fetch()
.then((message) => {
console.log(message);
})
.catch((error) => {
console.log(error);
});
89 changes: 89 additions & 0 deletions spec/cluster/orgs_api.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
jest.setTimeout(15000);

import twilio from "twilio";

const clientId = process.env.TWILIO_ORGS_CLIENT_ID;
const clientSecret = process.env.TWILIO_ORGS_CLIENT_SECRET;
const organizationSid = process.env.TWILIO_ORG_SID;
const accountSid = process.env.TWILIO_ACCOUNT_SID;
const userId = process.env.TWILIO_ORGS_USER_ID;

const client = twilio();
const orgsCredentialProvider = new twilio.OrgsCredentialProviderBuilder()
.setClientId(clientId)
.setClientSecret(clientSecret)
.build();
client.setCredentialProvider(orgsCredentialProvider);

test("Should generate access token", () => {
const noAuthClient = twilio();
noAuthClient.setCredentialProvider(new twilio.NoAuthCredentialProvider());
return noAuthClient.previewIam.v1.token
.create({
grantType: "client_credentials",
clientId: clientId,
clientSecret: clientSecret,
})
.then((token) => {
expect(token).not.toBeNull();
expect(token.accessToken).not.toBeUndefined();
expect(token.tokenType).toEqual("Bearer");
expect(token.expiresIn).toEqual(86400);
});
});

test("Should list accounts under an organization", () => {
return client.previewIam
.organization(organizationSid)
.accounts.list()
.then((accounts) => {
expect(accounts).not.toBeNull();
expect(accounts).not.toBeUndefined();
expect(accounts.length).toBeGreaterThanOrEqual(0);
});
});

test("Should fetch given account", () => {
return client.previewIam
.organization(organizationSid)
.accounts(accountSid)
.fetch()
.then((account) => {
expect(account).not.toBeNull();
expect(account).not.toBeUndefined();
expect(account.accountSid).toEqual(accountSid);
});
});

test("Should list users", () => {
return client.previewIam
.organization(organizationSid)
.users.list()
.then((users) => {
expect(users).not.toBeNull();
expect(users).not.toBeUndefined();
expect(users.length).toBeGreaterThanOrEqual(0);
});
});

test("Should fetch given user", () => {
return client.previewIam
.organization(organizationSid)
.users(userId)
.fetch()
.then((user) => {
expect(user).not.toBeNull();
expect(user).not.toBeUndefined();
expect(user.id).toEqual(userId);
});
});

test("Should list role assignments", () => {
client.previewIam
.organization(organizationSid)
.roleAssignments.list({ scope: accountSid })
.then((roles) => {
expect(roles).not.toBeNull();
expect(roles.length).toBeGreaterThanOrEqual(0);
});
});
28 changes: 28 additions & 0 deletions spec/cluster/public_oauth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
jest.setTimeout(15000);

import twilio from "twilio";

const clientId = process.env.TWILIO_CLIENT_ID;
const clientSecret = process.env.TWILIO_CLIENT_SECRET;
const accountSid = process.env.TWILIO_ACCOUNT_SID;

const clientCredentialProvider = new twilio.ClientCredentialProviderBuilder()
.setClientId(clientId)
.setClientSecret(clientSecret)
.build();

const client = twilio();
client.setCredentialProvider(clientCredentialProvider);
client.setAccountSid(accountSid);

test("Should fetch message", () => {
const messageId = process.env.TWILIO_MESSAGE_SID;
return client
.messages(messageId)
.fetch()
.then((message) => {
expect(message).not.toBeNull();
expect(message).not.toBeUndefined();
expect(message.sid).toEqual(messageId);
});
});
23 changes: 23 additions & 0 deletions spec/unit/auth_strategy/BasicAuthStrategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import BasicAuthStrategy from "../../../src/auth_strategy/BasicAuthStrategy";

describe("BasicAuthStrategy constructor", function () {
const username = "username";
const password = "password";
const basicAuthStrategy = new BasicAuthStrategy(username, password);

it("Should have basic as its authType", function () {
expect(basicAuthStrategy.getAuthType()).toEqual("basic");
});

it("Should return basic auth string", function (done) {
const auth = Buffer.from(username + ":" + password).toString("base64");
basicAuthStrategy.getAuthString().then(function (authString) {
expect(authString).toEqual(`Basic ${auth}`);
done();
});
});

it("Should return true for requiresAuthentication", function () {
expect(basicAuthStrategy.requiresAuthentication()).toBe(true);
});
});
20 changes: 20 additions & 0 deletions spec/unit/auth_strategy/NoAuthStrategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import NoAuthStrategy from "../../../src/auth_strategy/NoAuthStrategy";

describe("NoAuthStrategy constructor", function () {
const noAuthStrategy = new NoAuthStrategy();

it("Should have noauth as its authType", function () {
expect(noAuthStrategy.getAuthType()).toEqual("noauth");
});

it("Should return an empty string for getAuthString", function (done) {
noAuthStrategy.getAuthString().then(function (authString) {
expect(authString).toEqual("");
done();
});
});

it("Should return false for requiresAuthentication", function () {
expect(noAuthStrategy.requiresAuthentication()).toBe(false);
});
});
126 changes: 126 additions & 0 deletions spec/unit/auth_strategy/TokenAuthStrategy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import TokenAuthStrategy from "../../../src/auth_strategy/TokenAuthStrategy";
import ApiTokenManager from "../../../src/http/bearer_token/ApiTokenManager";
import { jest } from "@jest/globals";
import axios from "axios";
import twilio from "../../../src";

function createMockAxios(promiseHandler: Promise<any>) {
const instance = () => promiseHandler;
instance.defaults = {
headers: {
post: {},
},
};
return instance;
}

describe("TokenAuthStrategy constructor", function () {
const clientId = "clientId";
const clientSecret = "clientSecret";
const grantType = "client_credentials";

const tokenManager = new ApiTokenManager({
grantType: grantType,
clientId: clientId,
clientSecret: clientSecret,
});
const tokenAuthStrategy = new TokenAuthStrategy(tokenManager);

let createSpy: jest.Spied<any>;
const initialHttpProxyValue = process.env.HTTP_PROXY;

beforeEach(() => {
createSpy = jest.spyOn(axios, "create");
createSpy.mockReturnValue(
createMockAxios(
Promise.resolve({
status: 200,
data: {
access_token: "accessTokenValue",
token_type: "Bearer",
},
})
)
);
});

afterEach(() => {
createSpy.mockRestore();

if (initialHttpProxyValue) {
process.env.HTTP_PROXY = initialHttpProxyValue;
} else {
delete process.env.HTTP_PROXY;
}
});

it("Should have token as its authType", function () {
expect(tokenAuthStrategy.getAuthType()).toEqual("token");
});

it("Should check token expiry", function () {
const accountSid = "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const keySid = "SKb5aed9ca12bf5890f37930e63cad6d38";
const token = new twilio.jwt.AccessToken(accountSid, keySid, "secret", {
identity: "ID@example.com",
});
expect(tokenAuthStrategy.isTokenExpired(token.toJwt())).toBe(false);
});

it("Should return token auth string", function (done) {
tokenAuthStrategy.getAuthString().then(function (authString) {
expect(authString).toEqual(`Bearer accessTokenValue`);
done();
});
});

it("Should return true for requiresAuthentication", function () {
expect(tokenAuthStrategy.requiresAuthentication()).toBe(true);
});
});

describe("TokenAuthStrategy error response", function () {
const clientId = "clientId";
const clientSecret = "clientSecret";
const grantType = "client_credentials";

const tokenManager = new ApiTokenManager({
grantType: grantType,
clientId: clientId,
clientSecret: clientSecret,
});
const tokenAuthStrategy = new TokenAuthStrategy(tokenManager);

let createSpy: jest.Spied<any>;
const initialHttpProxyValue = process.env.HTTP_PROXY;

beforeEach(() => {
createSpy = jest.spyOn(axios, "create");
createSpy.mockReturnValue(
createMockAxios(
Promise.resolve({
status: 403,
data: {
message: "Invalid Credentials",
},
})
)
);
});

afterEach(() => {
createSpy.mockRestore();

if (initialHttpProxyValue) {
process.env.HTTP_PROXY = initialHttpProxyValue;
} else {
delete process.env.HTTP_PROXY;
}
});

it("Should return error", async function () {
await expect(tokenAuthStrategy.getAuthString()).rejects.toThrow(
"Failed to fetch access token: Invalid Credentials"
);
});
});
Loading

0 comments on commit 10a9474

Please sign in to comment.